Core
Core
Core
Introduction
What's new
What's new
What's new
Get started
Create a web app
Create a Web API
Tutorials
Create a Razor Pages web app
Get started with Razor Pages
Add a model
Scaffolded Razor Pages
SQL Server LocalDB
Update the pages
Add search
Add a new field
Add validation
Create a real-time SignalR web app
Create a SignalR web app with TypeScript
Create an MVC web app
Get started
Add a controller
Add a view
Add a model
Work with SQL Server LocalDB
Controller methods and views
Add search
Add a new field
Add validation
Examine the Details and Delete methods
Build Web APIs
Create a Web API in Visual Studio Code
Create a Web API in Visual Studio for Mac
Create a Web API in Visual Studio
Create backend services for native mobile apps
Help pages using Swagger
Get started with NSwag
Get started with Swashbuckle
Data access - with EF Core
Data access - with Razor Pages and EF Core
Get started
Create, Read, Update, and Delete operations
Sort, filter, page, and group
Migrations
Create a complex data model
Read related data
Update related data
Handle concurrency conflicts
Data access - MVC with EF Core
Get started
Create, Read, Update, and Delete operations
Sort, filter, page, and group
Migrations
Create a complex data model
Read related data
Update related data
Handle concurrency conflicts
Inheritance
Advanced topics
Cross platform tutorials
Razor Pages web app on macOS
Get started with Razor Pages
Add a model
Scaffolded Razor Pages
Work with SQLite
Update the pages
Add search
Razor Pages web app with VS Code
Get started with Razor Pages
Add a model
Scaffolded Razor Pages
Work with SQLite
Update the pages
Add search
MVC web app with Visual Studio for Mac
Get started
Add a controller
Add a view
Add a model
Work with SQLite
Controller methods and views
Add search
Add a new field
Add validation
Examine the Details and Delete methods
MVC web app with Visual Studio Code on macOS or Linux
Get started
Add a controller
Add a view
Add a model
Work with SQLite
Controller methods and views
Add search
Add a new field
Add validation
Examine the Details and Delete methods
Web API with Visual Studio for Mac
Web API with Visual Studio Code
Develop apps using a file watcher
Create backend services for mobile apps
Fundamentals
Application startup
Dependency injection (services)
Middleware
Middleware
Factory-based middleware
Factory-based middleware with third-party container
Static files
Routing
URL rewriting middleware
Use multiple environments
Configuration and options
Configuration
Options
Logging
Logging with LoggerMessage
Handle errors
Host
Web Host
Generic Host
Background tasks with hosted services
Enhance an app from an external assembly
Servers
Kestrel
ASP.NET Core Module
HTTP.sys
Session and app state
File Providers
Repository pattern
Globalization and localization
Configure Portable Object localization with Orchard Core
Initiate HTTP requests
Request Features
Access HttpContext
Primitives
Change tokens
Open Web Interface for .NET (OWIN)
WebSockets
Microsoft.AspNetCore.App metapackage
Microsoft.AspNetCore.All metapackage
Choose between .NET Core and .NET Framework
Choose between ASP.NET Core and ASP.NET
Razor Pages
Filter methods for Razor Pages
Create a Razor Class Library
Route and app conventions
Upload files to a Razor Page
Razor SDK
MVC
Model binding
Model validation
Views
Razor syntax
View compilation
Layout
Tag Helpers
Create Tag Helpers
Use Tag Helpers in forms
Tag Helper Components
Built-in Tag Helpers
Anchor Tag Helper
Cache Tag Helper
Distributed Cache Tag Helper
Environment Tag Helper
Form Tag Helper
Image Tag Helper
Input Tag Helper
Label Tag Helper
Partial Tag Helper
Select Tag Helper
Textarea Tag Helper
Validation Message Tag Helper
Validation Summary Tag Helper
Partial views
Dependency injection into views
View components
Controllers
Route to controller actions
File uploads
Dependency injection into controllers
Test controllers
Advanced
Work with the app model
Filters
Areas
Application parts
Custom model binding
Compatibility version
Web API
Controller action return types
Controller action return types
Advanced
Custom formatters
Format response data
SignalR
Introduction
Get started
Server concepts
Supported platforms
Hubs
HubContext
Users and groups
Publish to Azure
Clients
.NET client
Java client
Java API reference
JavaScript client
JavaScript API reference
WebPack and TypeScript
Configuration
Authentication and authorization
Security considerations
MessagePack Hub Protocol
Streaming
Differences between SignalR versions
Test, debug, and troubleshoot
Unit testing
Integration tests
Razor Pages unit tests
Test controllers
Remote debugging
Snapshot debugging
Snapshot debugging in Visual Studio
Troubleshoot
Data access with EF Core
Get started with Razor Pages and EF Core using Visual Studio
Get started with ASP.NET Core and EF Core using Visual Studio
ASP.NET Core with EF Core - new database
ASP.NET Core with EF Core - existing database
Get started with ASP.NET Core and Entity Framework 6
Azure Storage
Add Azure Storage by using Visual Studio Connected Services
Get started with Blob storage and Visual Studio Connected Services
Get Started with Queue Storage and Visual Studio Connected Services
Get Started with Table Storage and Visual Studio Connected Services
Azure guidance
DevOps with ASP.NET Core and Azure
Introduction
Tools and downloads
Deploy to App Service
Continuous integration and deployment
Monitor and troubleshoot
Next steps
Client-side development
Use Gulp
Use Grunt
Use LibMan
LibMan CLI
LibMan in Visual Studio
Manage client-side packages with Bower
Build responsive sites with Bootstrap
Style apps with LESS, Sass, and Font Awesome
Bundle and minify
Use Browser Link
Use JavaScriptServices for SPAs
Use the SPA project templates
Angular project template
React project template
React with Redux project template
Mobile
Create backend services for native mobile apps
Host and deploy
Host on Azure App Service
Publish to Azure with Visual Studio
Publish to Azure with CLI tools
Continuous deployment to Azure with Visual Studio and Git
Continuous deployment to Azure with Azure Pipelines
Troubleshoot ASP.NET Core on Azure App Service
Host on Windows with IIS
Troubleshoot ASP.NET Core on IIS
ASP.NET Core Module configuration reference
Development-time IIS support in Visual Studio for ASP.NET Core
IIS Modules with ASP.NET Core
Host in a Windows service
Host on Linux with Nginx
Host on Linux with Apache
Host in Docker
Build Docker images
Visual Studio Tools for Docker
Publish to a Docker image
Proxy and load balancer configuration
Host in a web farm
Visual Studio publish profiles
Directory structure
Common errors reference for Azure App Service and IIS
Security
Authentication
Authentication
Introduction to Identity
Scaffold Identity
Add custom user data to Identity
Customize Identity
Community OSS authentication options
Configure Identity
Configure Windows Authentication
Custom storage providers for Identity
External providers
Facebook authentication
Twitter authentication
Google authentication
Microsoft authentication
Other authentication providers
Additional claims
WS-Federation authentication
Account confirmation and password recovery
Enable QR code generation in Identity
Two-factor authentication with SMS
Use Cookie Authentication without Identity
Azure Active Directory
Integrate Azure AD Into an ASP.NET Core web app
Integrate Azure AD B2C into a customer-facing ASP.NET Core web app
Integrate Azure AD B2C into an ASP.NET Core web API
Call a ASP.NET Core Web API from a WPF app using Azure AD
Call a Web API in an ASP.NET Core web app using Azure AD
Secure ASP.NET Core apps with IdentityServer4
Secure ASP.NET Core apps with Azure App Service authentication (Easy Auth)
Individual user accounts
Authorization
Introduction
Create an app with user data protected by authorization
Razor Pages authorization
Simple authorization
Role-based authorization
Claims-based authorization
Policy-based authorization
Authorization policy providers
Dependency injection in requirement handlers
Resource-based authorization
View-based authorization
Limit identity by scheme
Data protection
Introduction to data protection
Get started with the Data Protection APIs
Consumer APIs
Consumer APIs overview
Purpose strings
Purpose hierarchy and multi-tenancy
Hash passwords
Limit the lifetime of protected payloads
Unprotect payloads whose keys have been revoked
Configuration
Configure data protection
Default settings
Machine-wide policy
Non-DI aware scenarios
Extensibility APIs
Core cryptography extensibility
Key management extensibility
Miscellaneous APIs
Implementation
Authenticated encryption details
Subkey derivation and authenticated encryption
Context headers
Key management
Key storage providers
Key encryption at rest
Key immutability and settings
Key storage format
Ephemeral data protection providers
Compatibility
Replace <machineKey> in ASP.NET
Enforce HTTPS
EU General Data Protection Regulation (GDPR) support
Safe storage of app secrets in development
Azure Key Vault configuration provider
Anti-request forgery
Prevent open redirect attacks
Prevent Cross-Site Scripting
Enable Cross-Origin Requests (CORS)
Share cookies among apps
IP safelist
Performance
Cache responses
Cache in-memory
Work with a distributed cache
Response caching
Response caching middleware
Response compression
Migration
ASP.NET Core 2.0 to 2.1
ASP.NET to ASP.NET Core
MVC
Web API
Configuration
Authentication and Identity
ClaimsPrincipal.Current
Membership to Identity
HTTP modules to middleware
ASP.NET Core 1.x to 2.0
Authentication and Identity
API reference
Contribute
Introduction to ASP.NET Core
9/20/2018 • 2 minutes to read • Edit Online
Next steps
For more information, see the following resources:
Get started with Razor Pages
ASP.NET Core tutorials
Publish an ASP.NET Core app to Azure with Visual Studio
ASP.NET Core fundamentals
The weekly ASP.NET community standup covers the team's progress and plans. It features new blogs and
third-party software.
What's new in ASP.NET Core 2.1
8/22/2018 • 6 minutes to read • Edit Online
This article highlights the most significant changes in ASP.NET Core 2.1, with links to relevant documentation.
SignalR
SignalR has been rewritten for ASP.NET Core 2.1. ASP.NET Core SignalR includes a number of improvements:
A simplified scale-out model.
A new JavaScript client with no jQuery dependency.
A new compact binary protocol based on MessagePack.
Support for custom protocols.
A new streaming response model.
Support for clients based on bare WebSockets.
For more information, see ASP.NET Core SignalR.
HTTPS
With the increased focus on security and privacy, enabling HTTPS for web apps is important. HTTPS enforcement
is becoming increasingly strict on the web. Sites that don’t use HTTPS are considered insecure. Browsers
(Chromium, Mozilla) are starting to enforce that web features must be used from a secure context. GDPR requires
the use of HTTPS to protect user privacy. While using HTTPS in production is critical, using HTTPS in development
can help prevent issues in deployment (for example, insecure links). ASP.NET Core 2.1 includes a number of
improvements that make it easier to use HTTPS in development and to configure HTTPS in production. For more
information, see Enforce HTTPS.
On by default
To facilitate secure website development, HTTPS is now enabled by default. Starting in 2.1, Kestrel listens on
https://localhost:5001 when a local development certificate is present. A development certificate is created:
As part of the .NET Core SDK first-run experience, when you use the SDK for the first time.
Manually using the new dev-certs tool.
Run dotnet dev-certs https --trust to trust the certificate.
HTTPS redirection and enforcement
Web apps typically need to listen on both HTTP and HTTPS, but then redirect all HTTP traffic to HTTPS. In 2.1,
specialized HTTPS redirection middleware that intelligently redirects based on the presence of configuration or
bound server ports has been introduced.
Use of HTTPS can be further enforced using HTTP Strict Transport Security Protocol (HSTS ). HSTS instructs
browsers to always access the site via HTTPS. ASP.NET Core 2.1 adds HSTS middleware that supports options for
max age, subdomains, and the HSTS preload list.
Configuration for production
In production, HTTPS must be explicitly configured. In 2.1, default configuration schema for configuring HTTPS for
Kestrel has been added. Apps can be configured to use:
Multiple endpoints including the URLs. For more information, see Kestrel web server implementation: Endpoint
configuration.
The certificate to use for HTTPS either from a file on disk or from a certificate store.
GDPR
ASP.NET Core provides APIs and templates to help meet some of the EU General Data Protection Regulation
(GDPR ) requirements. For more information, see GDPR support in ASP.NET Core. A sample app shows how to
use and lets you test most of the GDPR extension points and APIs added to the ASP.NET Core 2.1 templates.
Integration tests
A new package is introduced that streamlines test creation and execution. The Microsoft.AspNetCore.Mvc.Testing
package handles the following tasks:
Copies the dependency file (*.deps) from the tested app into the test project's bin folder.
Sets the content root to the tested app's project root so that static files and pages/views are found when the
tests are executed.
Provides the WebApplicationFactory class to streamline bootstrapping the tested app with TestServer.
The following test uses xUnit to check that the Index page loads with a success status code and with the correct
Content-Type header:
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;
[Fact]
public async Task GetHomePage()
{
// Act
var response = await _client.GetAsync("/");
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
[ApiController], ActionResult<T>
ASP.NET Core 2.1 adds new programming conventions that make it easier to build clean and descriptive web
APIs. ActionResult<T> is a new type added to allow an app to return either a response type or any other action
result (similar to IActionResult), while still indicating the response type. The [ApiController] attribute has also
been added as the way to opt in to Web API-specific conventions and behaviors.
For more information, see Build Web APIs with ASP.NET Core.
IHttpClientFactory
ASP.NET Core 2.1 includes a new IHttpClientFactory service that makes it easier to configure and consume
instances of HttpClient in apps. HttpClient already has the concept of delegating handlers that could be linked
together for outgoing HTTP requests. The factory:
Makes registering of instances of HttpClient per named client more intuitive.
Implements a Polly handler that allows Polly policies to be used for Retry, CircuitBreakers, etc.
For more information, see Initiate HTTP Requests.
Additional information
For the complete list of changes, see the ASP.NET Core 2.1 Release Notes.
What's new in ASP.NET Core 2.0
8/20/2018 • 5 minutes to read • Edit Online
This article highlights the most significant changes in ASP.NET Core 2.0, with links to relevant documentation.
Razor Pages
Razor Pages is a new feature of ASP.NET Core MVC that makes coding page-focused scenarios easier and more
productive.
For more information, see the introduction and tutorial:
Introduction to Razor Pages
Get started with Razor Pages
Runtime Store
Applications that use the Microsoft.AspNetCore.All metapackage automatically take advantage of the new .NET
Core Runtime Store. The Store contains all the runtime assets needed to run ASP.NET Core 2.0 applications. When
you use the Microsoft.AspNetCore.All metapackage, no assets from the referenced ASP.NET Core NuGet
packages are deployed with the application because they already reside on the target system. The assets in the
Runtime Store are also precompiled to improve application startup time.
For more information, see Runtime store
Configuration update
An IConfiguration instance is added to the services container by default in ASP.NET Core 2.0. IConfiguration in
the services container makes it easier for applications to retrieve configuration values from the container.
For information about the status of planned documentation, see the GitHub issue.
Logging update
In ASP.NET Core 2.0, logging is incorporated into the dependency injection (DI) system by default. You add
providers and configure filtering in the Program.cs file instead of in the Startup.cs file. And the default
ILoggerFactory supports filtering in a way that lets you use one flexible approach for both cross-provider filtering
and specific-provider filtering.
For more information, see Introduction to Logging.
Authentication update
A new authentication model makes it easier to configure authentication for an application using DI.
New templates are available for configuring authentication for web apps and web APIs using [Azure AD B2C ]
(https://azure.microsoft.com/services/active-directory-b2c/).
For information about the status of planned documentation, see the GitHub issue.
Identity update
We've made it easier to build secure web APIs using Identity in ASP.NET Core 2.0. You can acquire access tokens
for accessing your web APIs using the Microsoft Authentication Library (MSAL ).
For more information on authentication changes in 2.0, see the following resources:
Account confirmation and password recovery in ASP.NET Core
Enable QR Code generation for authenticator apps in ASP.NET Core
Migrate Authentication and Identity to ASP.NET Core 2.0
SPA templates
Single Page Application (SPA) project templates for Angular, Aurelia, Knockout.js, React.js, and React.js with Redux
are available. The Angular template has been updated to Angular 4. The Angular and React templates are available
by default; for information about how to get the other templates, see Create a new SPA project. For information
about how to build a SPA in ASP.NET Core, see Use JavaScriptServices for Creating Single Page Applications.
Kestrel improvements
The Kestrel web server has new features that make it more suitable as an Internet-facing server. A number of
server constraint configuration options are added in the KestrelServerOptions class's new Limits property. Add
limits for the following:
Maximum client connections
Maximum request body size
Minimum request body data rate
For more information, see Kestrel web server implementation in ASP.NET Core.
For more information, see HTTP.sys web server implementation in ASP.NET Core.
The file returned to your visitors will be decorated with the appropriate HTTP headers for the ETag and
LastModified values.
If an application visitor requests content with a Range Request header, ASP.NET Core recognizes the request and
handles the header. If the requested content can be partially delivered, ASP.NET Core appropriately skips and
returns just the requested set of bytes. You don't need to write any special handlers into your methods to adapt or
handle this feature; it's automatically handled for you.
Automatic precompilation
Razor view pre-compilation is enabled during publish by default, reducing the publish output size and application
startup time.
For more information, see Razor view compilation and precompilation in ASP.NET Core.
<LangVersion>latest</LangVersion>
For information about the status of C# 7.1 features, see the Roslyn GitHub repository.
Other documentation updates for 2.0
Visual Studio publish profiles for ASP.NET Core app deployment
Key Management
Configure Facebook authentication
Configure Twitter authentication
Configure Google authentication
Configure Microsoft Account authentication
Migration guidance
For guidance on how to migrate ASP.NET Core 1.x applications to ASP.NET Core 2.0, see the following resources:
Migrate from ASP.NET Core 1.x to ASP.NET Core 2.0
Migrate Authentication and Identity to ASP.NET Core 2.0
Additional Information
For the complete list of changes, see the ASP.NET Core 2.0 Release Notes.
To connect with the ASP.NET Core development team's progress and plans, tune in to the ASP.NET Community
Standup.
What's new in ASP.NET Core 1.1
8/20/2018 • 2 minutes to read • Edit Online
Additional Information
ASP.NET Core 1.1.0 Release Notes
To connect with the ASP.NET Core development team's progress and plans, tune in to the ASP.NET
Community Standup.
Get started with ASP.NET Core
9/21/2018 • 2 minutes to read • Edit Online
```console
dotnet dev-certs https --trust
```
cd aspnetcoreapp
dotnet run
5. Browse to http://localhost:5001. Click Accept to accept the privacy and cookie policy. This app doesn't keep
personal information.
6. Open Pages/About.cshtml and modify the page with the following highlighted markup:
@page
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>
cd aspnetcoreapp
dotnet run
4. Browse to http://localhost:5000.
5. Open Pages/About.cshtml and modify the page to display the message "Hello, world! The time on the
server is @DateTime.Now":
@page
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>
mkdir aspnetcoreapp
cd aspnetcoreapp
3. If you have installed a later SDK version on your machine, create a global.json file to select the 1.0.4 SDK.
{
"sdk": { "version": "1.0.4" }
}
dotnet restore
dotnet run
Prerequisites
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac
Razor Pages
Razor Pages is enabled in Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}
@page
<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>
The preceding code looks a lot like a Razor view file. What makes it different is the @page directive. @page makes
the file into an MVC action - which means that it handles requests directly, without going through a controller.
@page must be the first Razor directive on a page. @page affects the behavior of other Razor constructs.
A similar page, using a PageModel class, is shown in the following two files. The Pages/Index2.cshtml file:
@page
@using RazorPagesIntro.Pages
@model IndexModel2
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace RazorPagesIntro.Pages
{
public class IndexModel2 : PageModel
{
public string Message { get; private set; } = "PageModel in C#";
By convention, the PageModel class file has the same name as the Razor Page file with .cs appended. For example,
the previous Razor Page is Pages/Index2.cshtml. The file containing the PageModel class is named
Pages/Index2.cshtml.cs.
The associations of URL paths to pages are determined by the page's location in the file system. The following table
shows a Razor Page path and the matching URL:
/Pages/Index.cshtml / or /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
Notes:
The runtime looks for Razor Pages files in the Pages folder by default.
Index is the default page when a URL doesn't include a page.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;
namespace RazorPagesContacts
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }
namespace RazorPagesContacts.Data
{
public class Customer
{
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
}
}
The db context:
using Microsoft.EntityFrameworkCore;
namespace RazorPagesContacts.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>
namespace RazorPagesContacts.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}
By convention, the PageModel class is called <PageName>Model and is in the same namespace as the page.
The PageModel class allows separation of the logic of a page from its presentation. It defines page handlers for
requests sent to the page and the data used to render the page. This separation allows you to manage page
dependencies through dependency injection and to unit test the pages.
The page has an OnPostAsync handler method, which runs on POST requests (when a user posts the form). You can
add handler methods for any HTTP verb. The most common handlers are:
OnGet to initialize state needed for the page. OnGet sample.
OnPost to handle form submissions.
The Async naming suffix is optional but is often used by convention for asynchronous functions. The OnPostAsync
code in the preceding example looks similar to what you would normally write in a controller. The preceding code is
typical for Razor Pages. Most of the MVC primitives like model binding, validation, and action results are shared.
The previous OnPostAsync method:
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The basic flow of OnPostAsync :
Check for validation errors.
If there are no errors, save the data and redirect.
If there are errors, show the page again with validation messages. Client-side validation is identical to traditional
ASP.NET Core MVC applications. In many cases, validation errors would be detected on the client, and never
submitted to the server.
When the data is entered successfully, the OnPostAsync handler method calls the RedirectToPage helper method to
return an instance of RedirectToPageResult . RedirectToPage is a new action result, similar to RedirectToAction or
RedirectToRoute , but customized for pages. In the preceding sample, it redirects to the root Index page ( /Index ).
RedirectToPage is detailed in the URL generation for Pages section.
When the submitted form has validation errors (that are passed to the server), the OnPostAsync handler method
calls the Page helper method. Page returns an instance of PageResult . Returning Page is similar to how actions
in controllers return View . PageResult is the default return type for a handler method. A handler method that
returns void renders the page.
The Customer property uses [BindProperty] attribute to opt in to model binding.
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
Razor Pages, by default, bind properties only with non-GET verbs. Binding to properties can reduce the amount of
code you have to write. Binding reduces code by using the same property to render form fields (
<input asp-for="Customer.Name" /> ) and accept the input.
NOTE
For security reasons, you must opt in to binding GET request data to page model properties. Verify user input before
mapping it to properties. Opting in to this behavior is useful when addressing scenarios which rely on query string or route
values.
To bind a property on GET requests, set the [BindProperty] attribute's SupportsGet property to true :
[BindProperty(SupportsGet = true)]
The home page (Index.cshtml):
@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h1>Contacts</h1>
<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model.Customers)
{
<tr>
<td>@contact.Id</td>
<td>@contact.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
</td>
</tr>
}
</tbody>
</table>
<a asp-page="./Create">Create</a>
</form>
namespace RazorPagesContacts.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
}
}
The Index.cshtml file contains the following markup to create an edit link for each contact:
The Anchor Tag Helper used the asp-route-{value} attribute to generate a link to the Edit page. The link contains
route data with the contact ID. For example, http://localhost:5000/Edit/1 .
The Pages/Edit.cshtml file:
@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Edit Customer";
}
<div>
<button type="submit">Save</button>
</div>
</form>
The first line contains the @page "{id:int}" directive. The routing constraint "{id:int}" tells the page to accept
requests to the page that contain int route data. If a request to the page doesn't contain route data that can be
converted to an int , the runtime returns an HTTP 404 (not found) error. To make the ID optional, append ? to
the route constraint:
@page "{id:int?}"
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
[BindProperty]
public Customer Customer { get; set; }
if (Customer == null)
{
return RedirectToPage("/Index");
}
return Page();
}
_db.Attach(Customer).State = EntityState.Modified;
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}
return RedirectToPage("/Index");
}
}
}
The Index.cshtml file also contains markup to create a delete button for each customer contact:
When the delete button is rendered in HTML, its formaction includes parameters for:
The customer contact ID specified by the asp-route-id attribute.
The handler specified by the asp-page-handler attribute.
When the button is selected, a form POST request is sent to the server. By convention, the name of the handler
method is selected based the value of the handler parameter according to the scheme OnPost[handler]Async .
Because the handler is delete in this example, the OnPostDeleteAsync handler method is used to process the
POST request. If the asp-page-handler is set to a different value, such as remove , a page handler method with the
name OnPostRemoveAsync is selected.
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
[Required(ErrorMessage = "Color is required")]
public string Color { get; set; }
// Process color.
return RedirectToPage("./Index");
}
}
}
If no HEAD handler ( OnHead ) is defined, Razor Pages falls back to calling the GET page handler ( OnGet ) in
ASP.NET Core 2.1 or later. Opt in to this behavior with the SetCompatibilityVersion method in Startup.Configure
for ASP.NET Core 2.1 to 2.x:
services.AddMvc()
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1);
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.AllowMappingHeadRequestsToGetHandler = true;
});
XSRF/CSRF and Razor Pages
You don't have to write any code for antiforgery validation. Antiforgery token generation and validation are
automatically included in Razor Pages.
Using Layouts, partials, templates, and Tag Helpers with Razor Pages
Pages work with all the capabilities of the Razor view engine. Layouts, partials, templates, Tag Helpers,
_ViewStart.cshtml, _ViewImports.cshtml work in the same way they do for conventional Razor views.
Let's declutter this page by taking advantage of some of those capabilities.
Add a layout page to Pages/Shared/_Layout.cshtml:
Add a layout page to Pages/_Layout.cshtml:
<!DOCTYPE html>
<html>
<head>
<title>Razor Pages Sample</title>
</head>
<body>
<a asp-page="/Index">Home</a>
@RenderBody()
<a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>
The Layout:
Controls the layout of each page (unless the page opts out of layout).
Imports HTML structures such as JavaScript and stylesheets.
See layout page for more information.
The Layout property is set in Pages/_ViewStart.cshtml:
@{
Layout = "_Layout";
}
The layout is in the Pages/Shared folder. Pages look for other views (layouts, templates, partials) hierarchically,
starting in the same folder as the current page. A layout in the Pages/Shared folder can be used from any Razor
page under the Pages folder.
The layout file should go in the Pages/Shared folder.
The layout is in the Pages folder. Pages look for other views (layouts, templates, partials) hierarchically, starting in
the same folder as the current page. A layout in the Pages folder can be used from any Razor page under the Pages
folder.
We recommend you not put the layout file in the Views/Shared folder. Views/Shared is an MVC views pattern.
Razor Pages are meant to rely on folder hierarchy, not path conventions.
View search from a Razor Page includes the Pages folder. The layouts, templates, and partials you're using with
MVC controllers and conventional Razor views just work.
Add a Pages/_ViewImports.cshtml file:
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@namespace is explained later in the tutorial. The @addTagHelper directive brings in the built-in Tag Helpers to all the
pages in the Pages folder.
When the @namespace directive is used explicitly on a page:
@page
@namespace RazorPagesIntro.Pages.Customers
@model NameSpaceModel
<h2>Name space</h2>
<p>
@Model.Message
</p>
The directive sets the namespace for the page. The @model directive doesn't need to include the namespace.
When the @namespace directive is contained in _ViewImports.cshtml, the specified namespace supplies the prefix for
the generated namespace in the Page that imports the @namespace directive. The rest of the generated namespace
(the suffix portion) is the dot-separated relative path between the folder containing _ViewImports.cshtml and the
folder containing the page.
For example, the PageModel class Pages/Customers/Edit.cshtml.cs explicitly sets the namespace:
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The generated namespace for the Pages/Customers/Edit.cshtml Razor Page is the same as the PageModel class.
@namespace also works with conventional Razor views.
The original Pages/Create.cshtml view file:
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>
@page
@model CreateModel
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>
The Razor Pages starter project contains the Pages/_ValidationScriptsPartial.cshtml, which hooks up client-side
validation.
For more information on partial views, see Partial views in ASP.NET Core.
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The page name is the path to the page from the root /Pages folder including a leading / (for example, /Index ).
The preceding URL generation samples offer enhanced options and functional capabilities over hardcoding a URL.
URL generation uses routing and can generate and encode parameters according to how the route is defined in the
destination path.
URL generation for pages supports relative names. The following table shows which Index page is selected with
different RedirectToPage parameters from Pages/Customers/Create.cshtml:
REDIRECTTOPAGE(X) PAGE
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index
ViewData attribute
Data can be passed to a page with ViewDataAttribute. Properties on controllers or Razor Page models decorated
with [ViewData] have their values stored and loaded from the ViewDataDictionary.
In the following example, the AboutModel contains a Title property decorated with [ViewData] . The Title
property is set to the title of the About page:
public class AboutModel : PageModel
{
[ViewData]
public string Title { get; } = "About";
<h1>@Model.Title</h1>
<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"] - WebApplication</title>
...
TempData
ASP.NET Core exposes the TempData property on a controller. This property stores data until it's read. The Keep
and Peek methods can be used to examine the data without deletion. TempData is useful for redirection, when data
is needed for more than a single request.
The [TempData] attribute is new in ASP.NET Core 2.0 and is supported on controllers and pages.
The following code sets the value of Message using TempData :
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;
[TempData]
public string Message { get; set; }
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}
The following markup in the Pages/Customers/Index.cshtml file displays the value of Message using TempData .
<h3>Msg: @Model.Message</h3>
The Pages/Customers/Index.cshtml.cs page model applies the [TempData] attribute to the Message property.
[TempData]
public string Message { get; set; }
@page
@model CreateFATHModel
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>
The form in the preceding example has two submit buttons, each using the FormActionTagHelper to submit to a
different URL. The asp-page-handler attribute is a companion to asp-page . asp-page-handler generates URLs that
submit to each of the handler methods defined by a page. asp-page isn't specified because the sample is linking to
the current page.
The page model:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages.Customers
{
public class CreateFATHModel : PageModel
{
private readonly AppDbContext _db;
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The preceding code uses named handler methods. Named handler methods are created by taking the text in the
name after On<HTTP Verb> and before Async (if present). In the preceding example, the page methods are
OnPostJoinListAsync and OnPostJoinListUCAsync. With OnPost and Async removed, the handler names are
JoinList and JoinListUC .
A root-relative path designated by a tilde ( ~ ) at the beginning of the path is supported. For example,
@page "~/Some/Other/Path" is the same as @page "/Some/Other/Path" .
You can change the query string ?handler=JoinList in the URL to a route segment /JoinList by specifying the
route template @page "{handler?}" .
If you don't like the query string ?handler=JoinList in the URL, you can change the route to put the handler name
in the path portion of the URL. You can customize the route by adding a route template enclosed in double quotes
after the @page directive.
@page "{handler?}"
@model CreateRouteModel
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>
Using the preceding code, the URL path that submits to OnPostJoinListAsync is
http://localhost:5000/Customers/CreateFATH/JoinList . The URL path that submits to OnPostJoinListUCAsync is
http://localhost:5000/Customers/CreateFATH/JoinListUC .
services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesAtContentRoot();
services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesRoot("/path/to/razor/pages");
Additional resources
Introduction to ASP.NET Core
Razor syntax reference for ASP.NET Core
Get started with Razor Pages in ASP.NET Core
Razor Pages authorization conventions in ASP.NET Core
Razor Pages route and app conventions in ASP.NET Core
Razor Pages unit tests in ASP.NET Core
Partial views in ASP.NET Core
Create a Web API with ASP.NET Core and Visual
Studio
8/9/2018 • 16 minutes to read • Edit Online
Overview
This tutorial creates the following API:
GET /api/todo Get all to-do items None Array of to-do items
The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a client.
Postman or curl is used as the client to test the app.
A model is an object that represents the data in the app. In this case, the only model is a to-do item. Models
are represented as C# classes, also known as Plain Old CLR Object (POCOs).
A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single
controller.
To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items in
an in-memory database.
Prerequisites
Visual Studio 2017 version 15.7.3 or later with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
["value1","value2"]
NOTE
The model classes can go anywhere in the project. The Models folder is used by convention for model classes.
In Solution Explorer, right-click the Models folder and select Add > Class. Name the class TodoItem and click Add.
Update the TodoItem class with the following code:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
The database generates the Id when a TodoItem is created.
Create the database context
The database context is the main class that coordinates Entity Framework functionality for a given data model. This
class is created by deriving from the Microsoft.EntityFrameworkCore.DbContext class.
In Solution Explorer, right-click the Models folder and select Add > Class. Name the class TodoContext and click
Add.
Replace the class with the following code:
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
The preceding code defines an API controller class without methods. In the next sections, methods are added to
implement the API.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
In the next sections, methods are added to implement the API. The class is annotated with an [ApiController]
attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate
class with ApiControllerAttribute.
The controller's constructor uses Dependency Injection to inject the database context ( TodoContext ) into the
controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an
item to the in-memory database if one doesn't exist.
[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each method
is constructed as follows:
Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which is the controller class name minus the "Controller"
suffix. For this sample, the controller class name is TodoController and the root name is "todo". ASP.NET Core
routing is case insensitive.
If the [HttpGet] attribute has a route template (such as [HttpGet("/products")] , append that to the path. This
sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.
If no item matches the requested ID, the method returns a 404 error. Returning NotFound returns an HTTP 404
response.
Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200
response.
Launch the app
In Visual Studio, press CTRL+F5 to launch the app. Visual Studio launches a browser and navigates to
http://localhost:<port>/api/values , where <port> is a randomly chosen port number. Navigate to the Todo
controller at http://localhost:<port>/api/todo .
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. The [FromBody] attribute
tells MVC to get the value of the to-do item from the body of the HTTP request.
[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. MVC gets the value of the
to-do item from the body of the HTTP request.
The CreatedAtRoute method:
Returns a 201 response. 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.
See 10.2.2 201 Created.
Uses the "GetTodo" named route to create the URL. The "GetTodo" named route is defined in GetById :
{
"name":"walk dog",
"isComplete":true
}
TIP
If no response displays after clicking Send, disable the SSL certification verification option. This is found under File >
Settings. Click the Send button again after disabling the setting.
Click the Headers tab in the Response pane and copy the Location header value:
The Location header URI can be used to access the new item.
Update
Add the following Update method:
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
Update is similar to Create , 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 deltas. To support
partial updates, use HTTP PATCH.
Use Postman to update the to-do item's name to "walk cat":
Delete
Add the following Delete method:
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following
markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following code:
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ 'display': 'block' });
}
$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open
launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to
open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a
complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the
experience richer. Below are explanations around the calls to the API.
Get a list of to -do items
To get a list of to-do items, send an HTTP GET request to /api/todo.
The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array.
This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url . GET is used
as the type . The success callback function is invoked if the request succeeds. In the callback, the DOM is updated
with the to-do information.
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
Next steps
For information on using a persistent database, see:
Create a Razor Pages web app with ASP.NET Core
Work with data in ASP.NET Core
ASP.NET Core Web API help pages using Swagger
Routing to controller actions
Build web APIs with ASP.NET Core
Controller action return types
For information about deploying an API, including to Azure App Service, see Host and deploy.
View or download sample code. See how to download.
ASP.NET Core tutorials
7/2/2018 • 2 minutes to read • Edit Online
The following step-by-step guides for developing ASP.NET Core applications are available:
Client-side development
Use Gulp
Use Grunt
Manage client-side packages with Bower
Build responsive sites with Bootstrap
Test
Unit testing in .NET Core using dotnet test
This series explains the basics of building a Razor Pages web app with ASP.NET Core using Visual Studio. Other
versions of this series include a macOS version and a Visual Studio Code version.
1. Get started with Razor Pages
2. Add a model to a Razor Pages app
3. Scaffolded Razor Pages
4. Work with SQL Server LocalDB
5. Updating the pages
6. Add search
7. Add a new field
8. Add validation
After the tutorial to add a file upload capability to the sample app, see Upload files to a Razor Page in ASP.NET
Core.
Tutorial: Get started with Razor Pages in ASP.NET
Core
9/26/2018 • 6 minutes to read • Edit Online
By Rick Anderson
We recommend you follow the ASP.NET Core 2.1 version of this tutorial. It's much easier to follow and
covers more features. Select ASP.NET Core 2.1 in the version selector.
This tutorial teaches the basics of building an ASP.NET Core Razor Pages web app. Razor Pages is the
recommended way to build UI for web apps in ASP.NET Core.
There are three versions of this tutorial:
Windows: This tutorial
MacOS: Get started with Razor Pages with Visual Studio for Mac
macOS, Linux, and Windows: Get started with ASP.NET Core Razor Pages in Visual Studio Code
View or download sample code (how to download)
Prerequisites
Visual Studio 2017 version 15.7.3 or later with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
appsettings.json Configuration
Prerequisites
Visual Studio 2017 version 15.7.3 or later with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
Select ASP.NET Core 2.0 in the dropdown, and then select Web Application.
NOTE
To use ASP.NET Core with .NET Framework, you must first select .NET Framework from the leftmost drop-
down in the dialog, then you can select the desired ASP.NET Core version.
Visual Studio starts IIS Express and runs your app. The address bar shows localhost:port# and not
something like example.com . That's because localhost is the standard hostname for your local computer.
Localhost only serves web requests from the local computer. When Visual Studio creates a web project, a
random port is used for the web server. In the preceding image, the port number is 5000. When you run
the app, you'll see a different port number.
Launching the app with Ctrl+F5 (non-debug mode) allows you to make code changes, save the file, refresh
the browser, and see the code changes. Many developers prefer to use non-debug mode to quickly launch
the app and view changes.
The default template creates RazorPagesMovie, Home, About and Contact links and pages. Depending on
the size of your browser window, you might need to click the navigation icon to show the links.
Test the links. The RazorPagesMovie and Home links go to the Index page. The About and Contact links
go to the About and Contact pages, respectively.
appsettings.json Configuration
The About , Contact and Index pages are basic pages you can use to start an app. The Error page is used
to display error information.
N E X T: A D D IN G A
M ODEL
Add a model to a Razor Pages app in ASP.NET Core
9/18/2018 • 8 minutes to read • Edit Online
By Rick Anderson
In this section, you add classes for managing movies in a database. You use these classes 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 you create are known as POCO classes (from "plain-old CLR objects") because they don't have
any dependency on EF Core. They define the properties of the data that are stored in the database.
In this tutorial, you write the model classes first, and EF Core creates the database. An alternate approach not
covered here is to generate model classes from an existing database.
View or download sample.
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Complete the Add Razor Pages using Entity Framework (CRUD ) dialog:
In the Model class drop down, select Movie (RazorPagesMovie.Models).
In the Data context class row, select the + (plus) sign and accept the generated name
RazorPagesMovie.Models.RazorPagesMovieContext.
In the Data context class drop down, select RazorPagesMovie.Models.RazorPagesMovieContext
Select Add.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
The main class that coordinates EF Core functionality for a given data model is the DB context class. The data
context is derived from Microsoft.EntityFrameworkCore.DbContext. The data context specifies which entities are
included in the data model. In this project, the class is named RazorPagesMovieContext .
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace RazorPagesMovie.Models
{
public class RazorPagesMovieContext : DbContext
{
public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet<Movie> property for the entity set. In Entity Framework terminology, an
entity set typically corresponds to a database table. An entity corresponds to a row in the table.
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
Alternatively, the following .NET Core CLI commands can be used from the project folder:
Ignore the following warning message, you fix that in a a later tutorial:
Microsoft.EntityFrameworkCore.Model.Validation[30000]
*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 'ForHasColumnType()'.*
The Add-Migration command generates code to create the initial database schema. The schema is based on the
model specified in the RazorPagesMovieContext (In the Data/RazorPagesMovieContext.cs file). The Initial
argument is used to name the migrations. You can use any name, but by convention you choose a name that
describes the migration. See Introduction to migrations for more information.
The Update-Database command runs the Up method in the Migrations/{time-stamp }_InitialCreate.cs file, which
creates the database.
If you get the error:
SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.
You missed the migrations step.
By Rick Anderson
In this section, you add classes for managing movies in a database. You use these classes 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 you create are known as POCO classes (from "plain-old CLR objects") because they don't have
any dependency on EF Core. They define the properties of the data that are stored in the database.
In this tutorial, you write the model classes first, and EF Core creates the database. An alternate approach not
covered here is to generate model classes from an existing database.
View or download sample.
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
using Microsoft.EntityFrameworkCore;
namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet property for the entity set. In Entity Framework terminology, an entity set
typically corresponds to a database table, and an entity corresponds to a row in the table.
Add a database connection string
Add a connection string to the appsettings.json file.
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
services.AddDbContext<MovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}
*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 'ForHasColumnType()'*
dotnet restore
dotnet aspnet-codegenerator razorpage -m Movie -dc MovieContext -udl -outDir Pages\Movies --
referenceScriptLibraries
The preceeding error happens when you are in the wrong directory. Open a command shell to the project
directory (The directory that contains the Program.cs, Startup.cs, and .csproj files), and then run the preceeding
command.
If you get the error:
PARAMETER DESCRIPTION
P R E V IO U S : G E T N E X T: S C A F F O L D E D R A Z O R
STA RTE D PAGES
Scaffolded Razor Pages in ASP.NET Core
8/1/2018 • 8 minutes to read • Edit Online
By Rick Anderson
This tutorial examines the Razor Pages created by scaffolding in the previous tutorial.
View or download sample.
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
Razor Pages are derived from PageModel . By convention, the PageModel -derived class is called <PageName>Model .
The constructor uses dependency injection to add the MovieContext to the page. All the scaffolded pages follow
this pattern. See Asynchronous code for more information on asynchronous programing with Entity Framework.
When a request is made for the page, the OnGetAsync method returns a list of movies to the Razor Page.
OnGetAsync or OnGet is called on a Razor Page to initialize the state for the page. In this case, OnGetAsync gets a
list of movies and displays them.
When OnGet returns void or OnGetAsync returns Task , no return method is used. When the return type is
IActionResult or Task<IActionResult> , a return statement must be provided. For example, the
Pages/Movies/Create.cshtml.cs OnPostAsync method:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Razor can transition from HTML into C# or into Razor-specific markup. When an @ symbol is followed by a
Razor reserved keyword, it transitions into Razor-specific markup, otherwise it transitions into C#.
The @page Razor directive makes the file into an MVC action — which means that it can handle requests. @page
must be the first Razor directive on a page. @page is an example of transitioning into Razor-specific markup. See
Razor syntax for more information.
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.Movie[0].Title))
The DisplayNameFor HTML Helper inspects the Title property referenced in the lambda expression to determine
the display name. The lambda expression is inspected rather than evaluated. That means there is no access
violation when model , model.Movie , or model.Movie[0] are null or empty. When the lambda expression is
evaluated (for example, with @Html.DisplayFor(modelItem => item.Title) ), the model's property values are
evaluated.
The @model directive
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
The directive specifies the type of the model passed to the Razor Page. In the preceding example, the
@model
@model line makes the PageModel -derived class available to the Razor Page. The model is used in the
@Html.DisplayNameFor and @Html.DisplayName HTML Helpers on the page.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
The preceding highlighted code is an example of Razor transitioning into C#. The { and } characters enclose a
block of C# code.
The PageModel base class has a ViewData dictionary property that can be used to add data that you want to pass
to a View. You add objects into the ViewData dictionary using a key/value pattern. In the preceding sample, the
"Title" property is added to the ViewData dictionary.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the Pages/Shared/_Layout.cshtml file.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the _Layout.cshtml file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
The line @*Markup removed for brevity.*@ is a Razor comment. Unlike HTML comments ( <!-- --> ), Razor
comments are not sent to the client.
Run the app and test the links in the project (Home, About, Contact, Create, Edit, and Delete). Each page sets
the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.
Pages/Index.cshtml and Pages/Movies/Index.cshtml currently have the same title, but you can modify them to
have different values.
NOTE
You may not be able to enter decimal commas in the Price field. To support jQuery validation for non-English locales that
use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. This
GitHub issue 4076 for instructions on adding decimal comma.
@{
Layout = "_Layout";
}
The preceding markup sets the layout file to Pages/Shared/_Layout.cshtml for all Razor files under the Pages
folder. See Layout for more information.
Update the layout
Change the <title> element in the Pages/Shared/_Layout.cshtml file to use a shorter string.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>
The preceding anchor element is a Tag Helper. In this case, it's the Anchor Tag Helper. The
asp-page="/Movies/Index" Tag Helper attribute and value creates a link to the /Movies/Index Razor Page.
Save your changes, and test the app by clicking on the RpMovie link. See the _Layout.cshtml file in GitHub.
The Create page model
Examine the Pages/Movies/Create.cshtml.cs page model:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
The method initializes any state needed for the page. The Create page doesn't have any state to initialize, so
OnGet
Page is returned. Later in the tutorial you see OnGet method initialize state. The Page method creates a
PageResult object that renders the Create.cshtml page.
The Movie property uses the [BindProperty] attribute to opt-in to model binding. When the Create form posts
the form values, the ASP.NET Core runtime binds the posted values to the Movie model.
The OnPostAsync method is run when the page posts form data:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be
caught on the client-side before the form is posted. An example of a model error is posting a value for the date
field that cannot be converted to a date. We'll talk more about client-side validation and model validation later in
the tutorial.
If there are no model errors, the data is saved, and the browser is redirected to the Index page.
The Create Razor Page
Examine the Pages/Movies/Create.cshtml Razor Page file:
@page
@model RazorPagesMovie.Pages.Movies.CreateModel
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<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-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Visual Studio displays the <form method="post"> tag in a distinctive font used for Tag Helpers:
The <form method="post"> element is a Form Tag Helper. The Form Tag Helper automatically includes an
antiforgery token.
The scaffolding engine creates Razor markup for each field in the model (except the ID ) similar to the following:
The Validation Tag Helpers ( <div asp-validation-summary and <span asp-validation-for ) display validation errors.
Validation is covered in more detail later in this series.
The Label Tag Helper ( <label asp-for="Movie.Title" class="control-label"></label> ) generates the label caption
and for attribute for the Title property.
The Input Tag Helper ( <input asp-for="Movie.Title" class="form-control" /> ) uses the DataAnnotations attributes
and produces HTML attributes needed for jQuery Validation on the client-side.
The next tutorial explains SQL Server LocalDB and seeding the database.
P R E V IO U S : A D D IN G A N E X T: S Q L S E R V E R
M ODEL LOCA LDB
Work with SQL Server LocalDB and ASP.NET Core
9/18/2018 • 4 minutes to read • Edit Online
services.AddDbContext<MovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
"ConnectionStrings": {
"MovieContext": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
When you deploy the app to a test or production server, you can use an environment variable or another approach
to set the connection string to a real SQL Server. See Configuration for more information.
namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new RazorPagesMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<RazorPagesMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the DB, the seed initializer returns and no movies are added.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
try
{
var context = services.GetRequiredService<MovieContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<RazorPagesMovieContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
A production app would not call Database.Migrate . It's added to the preceding code to prevent the following
exception when Update-Database has not been run:
SqlException: Cannot open database "RazorPagesMovieContext-21" requested by the login. The login failed.
Login failed for user 'user name'.
Test the app
Delete all the records in the DB. You can do this with the delete links in the browser or from SSOX
Force the app to initialize (call the methods in the Startup class) so the seed method runs. To force
initialization, IIS Express must be stopped and restarted. You can do this with any of the following
approaches:
Right click the IIS Express system tray icon in the notification area and tap Exit or Stop Site:
If you were running VS in non-debug mode, press F5 to run in debug mode.
If you were running VS in debug mode, stop the debugger and press F5.
The app shows the seeded data:
P R E V IO U S : S C A F F O L D E D R A Z O R N E X T: U P D A T IN G T H E
PAGES PAGES
Update the generated pages in an ASP.NET Core app
9/18/2018 • 5 minutes to read • Edit Online
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal. We don't want to see the time (12:00:00
AM in the image below ) and ReleaseDate should be Release Date (two words).
using System;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
Right click on a red squiggly line > Quick Actions and Refactorings.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
We'll cover DataAnnotations in the next tutorial. The Display attribute specifies what to display for the name of a
field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data
(Date), so the time information stored in the field isn't displayed.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml file.
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
preceding code, the AnchorTagHelper dynamically generates the HTML href attribute value from the Razor Page
(the route is relative), the asp-page , and the route id ( asp-route-id ). See URL generation for Pages for more
information.
Use View Source from your favorite browser to examine the generated markup. A portion of the generated
HTML is shown below:
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>
The dynamically-generated links pass the movie ID with a query string (for example,
http://localhost:5000/Movies/Details?id=2 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive for
each of these pages from @page to @page "{id:int}" . Run the app and then view source. The generated HTML
adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the "{id:int}" route template that does not include the integer will return an HTTP 404
(not found) error. For example, http://localhost:5000/Movies/Details will return a 404 error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code only detects concurrency exceptions when the first concurrent client deletes the movie, and the
second concurrent client posts changes to the movie.
To test the catch block:
Set a breakpoint on catch (DbUpdateConcurrencyException)
Edit a movie.
In another browser window, select the Delete link for the same movie, and then delete the movie.
In the previous browser window, post changes to the movie.
Production code would generally detect concurrency conflicts when two or more clients concurrently updated a
record. See Handle concurrency conflicts for more information.
Posting and binding review
Examine the Pages/Movies/Edit.cshtml.cs file:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
When an HTTP GET request is made to the Movies/Edit page (for example, http://localhost:5000/Movies/Edit/2 ):
The OnGetAsync method fetches the movie from the database and returns the Page method.
The Page method renders the Pages/Movies/Edit.cshtml Razor Page. The Pages/Movies/Edit.cshtml file
contains the model directive ( @model RazorPagesMovie.Pages.Movies.EditModel ), which makes the movie model
available on the page.
The Edit form is displayed with the values from the movie.
When the Movies/Edit page is posted:
The form values on the page are bound to the Movie property. The [BindProperty] attribute enables
Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state (for example, ReleaseDate cannot be converted to a date), the form is
posted again with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST
OnPostAsync method in the Create Razor Page follows a similar pattern to the OnPostAsync method in the Edit
Razor Page.
Search is added in the next tutorial.
P R E V IO U S : W O R K IN G W IT H S Q L S E R V E R ADD
LOCA LDB SE A RCH
Add search to ASP.NET Core Razor Pages
9/18/2018 • 3 minutes to read • Edit Online
By Rick Anderson
In this document, search capability is added to the Index page that enables searching movies by genre or name.
Update the Index page's OnGetAsync method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the OnGetAsync method creates a LINQ query to select the movies:
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 search string:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code is a Lambda Expression. Lambdas are used in method-based LINQ queries as
arguments to standard query operator methods such as the Where method or Contains (used in the preceding
code). LINQ queries are not executed when they're defined or when they're modified by calling a method (such as
Where , Contains or OrderBy ). Rather, query execution is deferred. That means the evaluation of an expression is
delayed until its realized value is iterated over or the ToListAsync method is called. See Query Execution for more
information.
Note: The Contains method is run on the database, not in the C# code. The case sensitivity on the query depends
on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case insensitive. In
SQLite, with the default collation, it's case sensitive.
Navigate to the Movies page and append a query string such as ?searchString=Ghost to the URL (for example,
http://localhost:5000/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, http://localhost:5000/Movies/Ghost ).
@page "{searchString?}"
The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query
string value. The ? in "{searchString?}" means this is an optional route parameter.
However, you can't expect users to modify the URL to search for a movie. In this step, UI is added to filter movies.
If you added the route constraint "{searchString?}" , remove it.
Open the Pages/Movies/Index.cshtml file, and add the <form> markup highlighted in the following code:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
The HTML <form> tag uses the Form Tag Helper. When the form is submitted, the filter string is sent to the
Pages/Movies/Index page. Save the changes and test the filter.
Search by genre
Add the following highlighted properties to Pages/Movies/Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
The SelectList Genres contains the list of genres. This allows the user to select a genre from the list.
The MovieGenre property contains the specific genre the user selects (for example, "Western").
Update the OnGetAsync method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}
The following code is a LINQ query that retrieves all the genres from the database.
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<table class="table">
<thead>
P R E V IO U S : U P D A T IN G T H E N E X T: A D D IN G A N E W
PAGES F IE L D
Add a new field to a Razor Page in ASP.NET Core
9/18/2018 • 4 minutes to read • Edit Online
By Rick Anderson
In this section you use Entity Framework Code First Migrations to add a new field to the model and migrate that
change to the database.
When using EF Code First to automatically create a database, Code First:
Adds a table to the database to track whether the schema of the database is in sync with the model classes it
was generated from.
If the model classes aren't in sync with the DB, EF throws an exception.
Automatic verification of schema/model in sync makes it easier to find inconsistent database/code issues.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<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>
Add the Rating field to the Delete and Details pages.
Update Create.cshtml with a Rating field. You can copy/paste the previous <div> element and let intelliSense
help you update the fields. IntelliSense works with Tag Helpers.
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<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">
<label asp-for="Movie.Rating" class="control-label"></label>
<input asp-for="Movie.Rating" class="form-control" />
<span asp-validation-for="Movie.Rating" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This error is caused by the updated Movie model class being different than the schema of the Movie table of the
database. (There's no Rating column in the database table.)
There are a few approaches to resolving the error:
1. Have the Entity Framework automatically drop and re-create the database using the new model class
schema. This approach is convenient early in the development cycle; it allows you to quickly evolve the
model and database schema together. The downside is that you lose existing data in the database. You don't
want to use this approach on a production database! Dropping the DB on schema changes and using an
initializer to automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage
of this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, use Code First Migrations.
Update the SeedData class so that it provides a value for the new column. A sample change is shown below, but
you'll want to make this change for each new Movie block.
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},
Add-Migration Rating
Update-Database
Update-Database
Run the app and verify you can create/edit/display movies with a Rating field. If the database isn't seeded, stop
IIS Express, and then run the app.
P R E V IO U S : A D D IN G N E X T: A D D IN G
SE A RCH V A L ID A T IO N
Add validation to an ASP.NET Core Razor Page
9/26/2018 • 8 minutes to read • Edit Online
By Rick Anderson
In this section, validation logic is added to the Movie model. The validation rules are enforced any time a user
creates or edits a movie.
Validation
A key tenet of software development is called DRY ("Don't Repeat Yourself"). Razor Pages encourages
development where functionality is specified once, and it's reflected throughout the app. DRY can help reduce the
amount of code in an app. DRY makes the code less error prone, and easier to test and maintain.
The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle.
Validation rules are declaratively specified in one place (in the model class), and the rules are enforced everywhere
in the app.
Adding validation rules to the movie model
Open the Movie.cs file. DataAnnotations provides a built-in set of validation attributes that are applied
declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType that help
with formatting and don't provide validation.
Update the Movie class to take advantage of the Required , StringLength , RegularExpression , and Range
validation attributes.
[Range(1, 100)]
[DataType(DataType.Currency)]
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; }
}
public class Movie
{
public int ID { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
Notice how the form has automatically rendered a validation error message in each field containing an invalid
value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (when a user has
JavaScript disabled).
A significant benefit is that no code changes were necessary in the Create or Edit pages. Once DataAnnotations
were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically
picked up the validation rules (using validation attributes on the properties of the Movie model class). Test
validation using the Edit page, the same validation is applied.
The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't
posted by one or more of the following approaches:
Put a break point in the OnPostAsync method. Submit the form (select Create or Save). The break point is
never hit.
Use the Fiddler tool.
Use the browser developer tools to monitor network traffic.
Server-side validation
When JavaScript is disabled in the browser, submitting the form with errors will post to the server.
Optional, test server-side validation:
Disable JavaScript in the browser. If you can't disable JavaScript in the browser, try another browser.
Set a break point in the OnPostAsync method of the Create or Edit page.
Submit a form with validation errors.
Verify the model state is invalid:
if (!ModelState.IsValid)
{
return Page();
}
The following code shows a portion of the Create.cshtml page that you scaffolded earlier in the tutorial. It's used
by the Create and Edit pages to display the initial form and to redisplay the form in the event of an error.
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery
Validation on the client-side. The Validation Tag Helper displays validation errors. See Validation for more
information.
The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified
only in the Movie class. These validation rules are automatically applied to Razor Pages that edit the Movie
model.
When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout
the application (validation logic is defined in one place). Validation in one place helps keep the code clean, and
makes it easier to maintain and update.
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
The DataType attributes only provide hints for the view engine to format the data (and supplies attributes such as
<a> for URL's and <a href="mailto:EmailAddress.com"> for email). Use the RegularExpression attribute to
validate the format of the data. The DataType attribute is used to specify a data type that's more specific than the
database intrinsic type. DataType attributes are not validation attributes. In the sample application, only the date is
displayed, without time.
The DataType Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency,
EmailAddress, and more. The DataType attribute can also enable the application to automatically provide type-
specific features. For example, a mailto: link can be created for DataType.EmailAddress . A date selector can be
provided for DataType.Date in browsers that support HTML5. The DataType attributes emits HTML 5 data-
(pronounced data dash) attributes that HTML 5 browsers consume. The DataType attributes do not provide any
validation.
DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed
according to the default formats based on the server's CultureInfo .
The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly
map Price to currency in the database. For more information, see Data Types.
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should be applied when the value is displayed for
editing. You might not want that behavior for some fields. For example, in currency values, you probably don't
want the currency symbol in the edit UI.
The DisplayFormat attribute can be used by itself, but it's generally a good idea to use the DataType attribute. The
DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the
following benefits that you don't get with DisplayFormat:
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, etc.)
By default, the browser will render data using the correct format based on your locale.
The DataType attribute can enable the ASP.NET Core framework to choose the right field template to render
the data. The DisplayFormat if used by itself uses the string template.
Note: jQuery validation doesn't work with the Range attribute and DateTime . For example, the following code will
always display a client-side validation error, even when the date is in the specified range:
It's generally not a good practice to compile hard dates in your models, so using the Range attribute and
DateTime is discouraged.
The following code shows combining attributes on one line:
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
Get started with Razor Pages and EF Core shows advanced EF Core operations with Razor Pages.
Publish to Azure
For information on deploying to Azure, See Tutorial: Build an ASP.NET app in Azure with SQL Database. The
instruction are for an ASP.NET app, not an ASP.NET Core app, but the steps are the same.
Thanks for completing this introduction to Razor Pages. We appreciate feedback. Get started with Razor Pages
and EF Core is an excellent follow up to this tutorial.
Additional resources
Working with Forms
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
P R E V IO U S : A D D IN G A N E W
F IE L D
Tutorial: Get started with SignalR on ASP.NET Core
9/27/2018 • 6 minutes to read • Edit Online
This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:
Create a web app that uses SignalR on ASP.NET Core.
Create a SignalR hub on the server.
Connect to the SignalR hub from JavaScript clients.
Use the hub to send messages from any client to all connected clients.
At the end, you'll have a working chat app:
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2017 version 15.8 or later with the ASP.NET and web development workload
.NET Core SDK 2.1 or later
Select Choose specific files, expand the dist/browser folder, and select signalr.js and signalr.min.js.
Set Target Location to wwwroot/lib/signalr/, and select Install.
LibMan creates a wwwroot/lib/signalr folder and copies the selected files to it.
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
The ChatHub class inherits from the SignalR Hub class. The Hub class manages connections, groups, and
messaging.
The SendMessage method can be called by any connected client. It sends the received message to all clients.
SignalR code is asynchronous to provide maximum scalability.
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a
given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request
pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
app.UseMvc();
}
}
}
These changes add SignalR to the dependency injection system and the middleware pipeline.
@page
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
User..........<input type="text" id="userInput" />
<br />
Message...<input type="text" id="messageInput" />
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
"use strict";
connection.start().catch(function (err) {
return console.error(err.toString());
});
TIP
If the app doesn't work, open your browser developer tools (F12) and go to the console. You might see errors related to your
HTML and JavaScript code. For example, suppose you put signalr.js in a different folder than directed. In that case the
reference to that file won't work and you'll see a 404 error in the console.
Next steps
If you want clients to connect to a SignalR app from different domains, you have to enable Cross-Origin Resource
Sharing (CORS ). For more information, see Cross-origin resource sharing.
To learn more about SignalR, hubs, and JavaScript clients, see these resources:
Introduction to SignalR for ASP.NET Core
Use hubs in SignalR for ASP.NET Core
ASP.NET Core SignalR JavaScript client
Use ASP.NET Core SignalR with TypeScript and
Webpack
7/19/2018 • 10 minutes to read • Edit Online
Prerequisites
Install the following software:
Visual Studio
.NET Core CLI
.NET Core SDK 2.1 or later
Node.js with npm
Visual Studio 2017 version 15.7.3 or later with the ASP.NET and web development workload
npm init -y
{
"name": "SignalRWebPack",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Setting the private property to true prevents package installation warnings in the next step.
3. Install the required npm packages. Execute the following command from the project root:
npm install -D -E [email protected] [email protected] [email protected] mini-css-
[email protected] [email protected] [email protected] [email protected] [email protected]
"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "wwwroot"),
filename: "[name].[chunkhash].js",
publicPath: "/"
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader"
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
plugins: [
new CleanWebpackPlugin(["wwwroot/*"]),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css"
})
]
};
The preceding file configures the Webpack compilation. Some configuration details to note:
The output property overrides the default value of dist. The bundle is instead emitted in the wwwroot
directory.
The resolve.extensions array includes .js to import the SignalR client JavaScript.
6. Create a new src directory in the project root. Its purpose is to store the project's client-side assets.
7. Create src/index.html with the following content.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR</title>
</head>
<body>
<div id="divMessages" class="messages">
</div>
<div class="input-zone">
<label id="lblMessage" for="tbMessage">Message:</label>
<input id="tbMessage" class="input-zone-input" type="text" />
<button id="btnSend">Send</button>
</div>
</body>
</html>
The preceding HTML defines the homepage's boilerplate markup.
8. Create a new src/css directory. Its purpose is to store the project's .css files.
9. Create src/css/main.css with the following content:
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
}
.input-zone {
align-items: center;
display: flex;
flex-direction: row;
margin: 10px;
}
.input-zone-input {
flex: 1;
margin-right: 10px;
}
.message-author {
font-weight: bold;
}
.messages {
border: 1px solid #000;
margin: 10px;
max-height: 300px;
min-height: 300px;
overflow-y: auto;
padding: 5px;
}
{
"compilerOptions": {
"target": "es5"
}
}
The preceding code configures the TypeScript compiler to produce ECMAScript 5-compatible JavaScript.
11. Create src/index.ts with the following content:
import "./css/main.css";
btnSend.addEventListener("click", send);
function send() {
}
The preceding TypeScript retrieves references to DOM elements and attaches two event handlers:
keyup : This event fires when the user types something in the textbox identified as tbMessage . The send
function is called when the user presses the Enter key.
click : This event fires when the user clicks the Send button. The send function is called.
app.UseDefaultFiles();
app.UseStaticFiles();
The preceding code allows the server to locate and serve the index.html file, whether the user enters its full
URL or the root URL of the web app.
2. Call AddSignalR in the Startup.ConfigureServices method. It adds the SignalR services to your project.
services.AddSignalR();
3. Map a /hub route to the ChatHub hub. Add the following lines at the end of the Startup.Configure method:
app.UseSignalR(options =>
{
options.MapHub<ChatHub>("/hub");
});
4. Create a new directory, called Hubs, in the project root. Its purpose is to store the SignalR hub, which is
created in the next step.
5. Create hub Hubs/ChatHub.cs with the following code:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
}
}
6. Add the following code at the top of the Startup.cs file to resolve the ChatHub reference:
using SignalRWebPack.Hubs;
The preceding command installs the SignalR TypeScript client, which allows the client to send messages to
the server.
2. Add the highlighted code to the src/index.ts file:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";
m.innerHTML =
`<div class="message__author">${username}</div><div>${message}</div>`;
divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});
btnSend.addEventListener("click", send);
function send() {
}
The preceding code supports receiving messages from the server. The HubConnectionBuilder class creates a
new builder for configuring the server connection. The withUrl function configures the hub URL.
SignalR enables the exchange of messages between a client and a server. Each message has a specific name.
For example, you can have messages with the name messageReceived that execute the logic responsible for
displaying the new message in the messages zone. Listening to a specific message can be done via the on
function. You can listen to any number of message names. It's also possible to pass parameters to the
message, such as the author's name and the content of the message received. Once the client receives a
message, a new div element is created with the author's name and the message content in its innerHTML
attribute. It's added to the main div element displaying the messages.
3. Now that the client can receive a message, configure it to send messages. Add the highlighted code to the
src/index.ts file:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";
messageContainer.innerHTML =
`<div class="message-author">${username}</div><div>${message}</div>`;
divMessages.appendChild(messageContainer);
divMessages.scrollTop = divMessages.scrollHeight;
});
btnSend.addEventListener("click", send);
function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => tbMessage.value = "");
}
Sending a message through the WebSockets connection requires calling the send method. The method's
first parameter is the message name. The message data inhabits the other parameters. In this example, a
message identified as newMessage is sent to the server. The message consists of the username and the user
input from a text box. If the send works, the text box value is cleared.
4. Add the highlighted method to the ChatHub class:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
public async Task NewMessage(string username, string message)
{
await Clients.All.SendAsync("messageReceived", username, message);
}
}
}
The preceding code broadcasts received messages to all connected users once the server receives them. It's
unnecessary to have a generic on method to receive all the messages. A method named after the message
name suffices.
In this example, the TypeScript client sends a message identified as newMessage . The C# NewMessage method
expects the data sent by the client. A call is made to the SendAsync method on Clients.All. The received
messages are sent to all clients connected to the hub.
This command yields the client-side assets to be served when running the app. The assets are placed in the
wwwroot folder.
Webpack completed the following tasks:
Purged the contents of the wwwroot directory.
Converted the TypeScript to JavaScript—a process known as transpilation.
Mangled the generated JavaScript to reduce file size—a process known as minification.
Copied the processed JavaScript, CSS, and HTML files from src to the wwwroot directory.
Injected the following elements into the wwwroot/index.html file:
A <link> tag, referencing the wwwroot/main.<hash>.css file. This tag is placed immediately
before the closing </head> tag.
A <script> tag, referencing the minified wwwroot/main.<hash>.js file. This tag is placed
immediately before the closing </body> tag.
2. Select Debug > Start without debugging to launch the app in a browser without attaching the debugger.
The wwwroot/index.html file is served at http://localhost:<port_number> .
3. Open another browser instance (any browser). Paste the URL in the address bar.
4. Choose either browser, type something in the Message text box, and click the Send button. The unique user
name and message are displayed on both pages instantly.
Additional resources
ASP.NET Core SignalR JavaScript client
Use hubs in ASP.NET Core SignalR
Create a web app with ASP.NET Core MVC on
Windows with Visual Studio
7/10/2018 • 2 minutes to read • Edit Online
This tutorial teaches ASP.NET Core MVC web development with controllers and views. Razor Pages is a feature of
the ASP.NET Core MVC framework that makes building and testing web UI easier and more productive. You can
use Razor pages alongside controllers and views in the same project.
We recommend you try the Razor Pages tutorial before the MVC/Controller/Views version. The Razor Pages
tutorial:
Is the preferred approach for new application development.
Is easier to follow.
Covers more features.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
There are 3 versions of this tutorial:
Windows: This series
macOS: Create an ASP.NET Core MVC app with Visual Studio for Mac
macOS, Linux, and Windows: Create an ASP.NET Core MVC app with Visual Studio Code
The tutorial series includes the following:
1. Get started
2. Add a controller
3. Add a view
4. Add a model
5. Work with SQL Server LocalDB
6. Controller methods and views
7. Add search
8. Add a new field
9. Add validation
10. Examine the Details and Delete methods
Get started with ASP.NET Core MVC and Visual
Studio
9/18/2018 • 5 minutes to read • Edit Online
By Rick Anderson
This tutorial teaches ASP.NET Core MVC web development with controllers and views. Razor Pages is a feature
of the ASP.NET Core MVC framework that makes building and testing web UI easier and more productive. You
can use Razor pages alongside controllers and views in the same project.
We recommend you try the Razor Pages tutorial before the MVC/Controller/Views version. The Razor Pages
tutorial:
Is the preferred approach for new application development.
Is easier to follow.
Covers more features.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
There are 3 versions of this tutorial:
macOS: Create an ASP.NET Core MVC app with Visual Studio for Mac
Windows: Create an ASP.NET Core MVC app with Visual Studio
macOS, Linux, and Windows: Create an ASP.NET Core MVC app with Visual Studio Code
Complete the New ASP.NET Core Web Application (.NET Core) - MvcMovie dialog:
In the version selector drop-down box select ASP.NET Core 2.1
Select Web Application(Model-View-Controller)
Tap OK.
Visual Studio used a default template for the MVC project you just created. You have a working app right now by
entering a project name and selecting a few options. This is a basic starter project, and it's a good place to start,
Tap F5 to run the app in debug mode or Ctrl-F5 in non-debug mode.
Visual Studio starts IIS Express and runs your app. Notice that the address bar shows localhost:port# and
not something like example.com . That's because localhost is the standard hostname for your local computer.
When Visual Studio creates a web project, a random port is used for the web server. In the image above, the
port number is 5000. The URL in the browser shows localhost:5000 . When you run the app, you'll see a
different port number.
Launching the app with Ctrl+F5 (non-debug mode) allows you to make code changes, save the file, refresh
the browser, and see the code changes. Many developers prefer to use non-debug mode to quickly launch the
app and view changes.
You can launch the app in debug or non-debug mode from the Debug menu item:
You can debug the app by tapping the IIS Express button
The default template gives you working Home, About and Contact links. The browser image above doesn't
show these links. Depending on the size of your browser, you might need to click the navigation icon to show
them.
If you were running in debug mode, tap Shift-F5 to stop debugging.
In the next part of this tutorial, we'll learn about MVC and start writing some code.
ASP.NET Core 2.x
ASP.NET Core 1.x
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac
Visual Studio starts IIS Express and runs your app. Notice that the address bar shows localhost:port# and
not something like example.com . That's because localhost is the standard hostname for your local computer.
When Visual Studio creates a web project, a random port is used for the web server. In the image above, the
port number is 5000. The URL in the browser shows localhost:5000 . When you run the app, you'll see a
different port number.
Launching the app with Ctrl+F5 (non-debug mode) allows you to make code changes, save the file, refresh
the browser, and see the code changes. Many developers prefer to use non-debug mode to quickly launch the
app and view changes.
You can launch the app in debug or non-debug mode from the Debug menu item:
You can debug the app by tapping the IIS Express button
The default template gives you working Home, About and Contact links. The browser image above doesn't
show these links. Depending on the size of your browser, you might need to click the navigation icon to show
them.
If you were running in debug mode, tap Shift-F5 to stop debugging.
In the next part of this tutorial, we'll learn about MVC and start writing some code.
NEXT
Add a controller to an ASP.NET Core MVC app
7/10/2018 • 5 minutes to read • Edit Online
By Rick Anderson
The Model-View -Controller (MVC ) architectural pattern separates an app into three main components: Model,
View, and Controller. The MVC pattern helps you create apps that are more testable and easier to update than
traditional monolithic apps. MVC -based apps contain:
Models: Classes that represent the data of the app. The model classes use validation logic to enforce
business rules for that data. Typically, model objects retrieve and store model state in a database. In this
tutorial, a Movie model retrieves movie data from a database, provides it to the view or updates it. Updated
data is written to a database.
Views: Views are the components that display the app's user interface (UI). Generally, this UI displays the
model data.
Controllers: Classes that handle browser requests. They retrieve model data and call view templates that
return a response. In an MVC app, the view only displays information; the controller handles and responds
to user input and interaction. For example, the controller handles route data and query-string values, and
passes these values to the model. The model might use these values to query the database. For example,
http://localhost:1234/Home/About has route data of Home (the controller ) and About (the action method to
call on the home controller). http://localhost:1234/Movies/Edit/5 is a request to edit the movie with ID=5
using the movie controller. We'll talk about route data later in the tutorial.
The MVC pattern helps you create apps that separate the different aspects of the app (input logic, business logic,
and UI logic), while providing a loose coupling between these elements. The pattern specifies where each kind of
logic should be located in the app. The UI logic belongs in the view. Input logic belongs in the controller. Business
logic belongs in the model. This separation helps you manage complexity when you build an app, because it
enables you to work on one aspect of the implementation at a time without impacting the code of another. For
example, you can work on the view code without depending on the business logic code.
We cover these concepts in this tutorial series and show you how to use them to build a movie app. The MVC
project contains folders for the Controllers and Views.
In Solution Explorer, right-click Controllers > Add > New Item
Select Controller Class
In the Add New Item dialog, enter HelloWorldController.
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
//
// GET: /HelloWorld/Welcome/
Every public method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a
string. Note the comments preceding each method.
An HTTP endpoint is a targetable URL in the web application, such as http://localhost:1234/HelloWorld , and
combines the protocol used: HTTP , the network location of the web server (including the TCP port):
localhost:1234 and the target URI HelloWorld .
The first comment states this is an HTTP GET method that's invoked by appending "/HelloWorld/" to the base
URL. The second comment specifies an HTTP GET method that's invoked by appending "/HelloWorld/Welcome/"
to the URL. Later on in the tutorial you'll use the scaffolding engine to generate HTTP POST methods.
Run the app in non-debug mode and append "HelloWorld" to the path in the address bar. The Index method
returns a string.
MVC invokes controller classes (and the action methods within them) depending on the incoming URL. The
default URL routing logic used by MVC uses a format like this to determine what code to invoke:
/[Controller]/[ActionName]/[Parameters]
You set the format for routing in the Configure method in Startup.cs file.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
When you run the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index"
method specified in the template line highlighted above.
The first URL segment determines the controller class to run. So localhost:xxxx/HelloWorld maps to the
HelloWorldController class. The second part of the URL segment determines the action method on the class. So
localhost:xxxx/HelloWorld/Index would cause the Index method of the HelloWorldController class to run. Notice
that you only had to browse to localhost:xxxx/HelloWorld and the Index method was called by default. This is
because Index is the default method that will be called on a controller if a method name isn't explicitly specified.
The third part of the URL segment ( id ) is for route data. You'll see route data later on in this tutorial.
Browse to http://localhost:xxxx/HelloWorld/Welcome . The Welcome method runs and returns the string "This is the
Welcome action method...". For this URL, the controller is HelloWorld and Welcome is the action method. You
haven't used the [Parameters] part of the URL yet.
Modify the code to pass some parameter information from the URL to the controller. For example,
/HelloWorld/Welcome?name=Rick&numtimes=4 . Change the Welcome method to include two parameters as shown in
the following code.
// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
(Replace xxxx with your port number.) You can try different values for name and numtimes in the URL. The MVC
model binding system automatically maps the named parameters from the query string in the address bar to
parameters in your method. See Model Binding for more information.
In the image above, the URL segment ( Parameters ) isn't used, the name and numTimes parameters are passed as
query strings. The ? (question mark) in the above URL is a separator, and the query strings follow. The &
character separates query strings.
Replace the Welcome method with the following code:
This time the third URL segment matched the route parameter id . The Welcome method contains a parameter
id that matched the URL template in the MapRoute method. The trailing ? (in id? ) indicates the id parameter
is optional.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
In these examples the controller has been doing the "VC" portion of MVC - that is, the view and controller work.
The controller is returning HTML directly. Generally you don't want controllers returning HTML directly, since that
becomes very cumbersome to code and maintain. Instead you typically use a separate Razor view template file to
help generate the HTML response. You do that in the next tutorial.
In Visual Studio, in non-debug mode (Ctrl+F5), you don't need to build the app after changing code. Just save the
file, refresh your browser and you can see the changes.
P R E V IO U S NEXT
Add a view to an ASP.NET Core MVC app
6/26/2018 • 8 minutes to read • Edit Online
By Rick Anderson
In this section you modify the HelloWorldController class to use Razor view template files to cleanly encapsulate
the process of generating HTML responses to a client.
You create a view template file using Razor. Razor-based view templates have a .cshtml file extension. They
provide an elegant way to create HTML output using C#.
Currently the method returns a string with a message that's hard-coded in the controller class. In the
Index
HelloWorldController class, replace the Index method with the following code:
The preceding code returns a View object. It uses a view template to generate an HTML response to the browser.
Controller methods (also known as action methods) such as the Index method above, generally return an
IActionResult (or a class derived from ActionResult ), not a type like string.
Right click on the Views folder, and then Add > New Folder and name the folder HelloWorld.
Right click on the Views/HelloWorld folder, and then Add > New Item.
In the Add New Item - MvcMovie dialog
In the search box in the upper-right, enter view
Tap Razor View
In the Name box, change the name if necessary to Index.cshtml.
Tap Add
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the following:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
If your browser window is small (for example on a mobile device), you might need to toggle (tap) the Bootstrap
navigation button in the upper right to see the Home, About, and Contact links.
Changing views and layout pages
Tap the menu links (MvcMovie, Home, About). Each page shows the same menu layout. The menu layout is
implemented in the Views/Shared/_Layout.cshtml file. Open the Views/Shared/_Layout.cshtml file.
Layout templates allow you to specify the HTML container layout of your site in one place and then apply it across
multiple pages in your site. Find the @RenderBody() line. RenderBody is a placeholder where all the view -specific
pages you create show up, wrapped in the layout page. For example, if you select the About link, the
Views/Home/About.cshtml view is rendered inside the RenderBody method.
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Movies" asp-action="Index" class="navbar-brand">Movie App</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - MvcMovie</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Movies" asp-action="Index" class="navbar-brand">Movie App</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - MvcMovie</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
WARNING
We haven't implemented the Movies controller yet, so if you click on that link, you'll get a 404 (Not found) error.
Save your changes and tap the About link. Notice how the title on the browser tab now displays About - Movie
App instead of About - Mvc Movie:
Tap the Contact link and notice that the title and anchor text also display Movie App. We were able to make the
change once in the layout template and have all pages on the site reflect the new link text and new title.
Examine the Views/_ViewStart.cshtml file:
@{
Layout = "_Layout";
}
The Views/_ViewStart.cshtml file brings in the Views/Shared/_Layout.cshtml file to each view. You can use the
Layout property to set a different layout view, or set it to null so no layout file will be used.
You'll make them slightly different so you can see which bit of code changes which part of the app.
@{
ViewData["Title"] = "Movie List";
}
ViewData["Title"] = "Movie List"; in the code above sets the Title property of the ViewData dictionary to
"Movie List". The Title property is used in the <title> HTML element in the layout page:
<title>@ViewData["Title"] - Movie App</title>
Save your change and navigate to http://localhost:xxxx/HelloWorld . Notice that the browser title, the primary
heading, and the secondary headings have changed. (If you don't see changes in the browser, you might be
viewing cached content. Press Ctrl+F5 in your browser to force the response from the server to be loaded.) The
browser title is created with ViewData["Title"] we set in the Index.cshtml view template and the additional "-
Movie App" added in the layout file.
Also notice how the content in the Index.cshtml view template was merged with the Views/Shared/_Layout.cshtml
view template and a single HTML response was sent to the browser. Layout templates make it really easy to make
changes that apply across all of the pages in your application. To learn more see Layout.
Our little bit of "data" (in this case the "Hello from our View Template!" message) is hard-coded, though. The MVC
application has a "V" (view ) and you've got a "C" (controller), but no "M" (model) yet.
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}
return View();
}
}
}
The ViewData dictionary object contains data that will be passed to the view.
Create a Welcome view template named Views/HelloWorld/Welcome.cshtml.
You'll create a loop in the Welcome.cshtml view template that displays "Hello" NumTimes . Replace the contents of
Views/HelloWorld/Welcome.cshtml with the following:
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
Data is taken from the URL and passed to the controller using the MVC model binder . The controller packages
the data into a ViewData dictionary and passes that object to the view. The view then renders the data as HTML to
the browser.
In the sample above, we used the ViewData dictionary to pass data from the controller to a view. Later in the
tutorial, we will use a view model to pass data from a controller to a view. The view model approach to passing
data is generally much preferred over the ViewData dictionary approach. See ViewModel vs ViewData vs
ViewBag vs TempData vs Session in MVC for more information.
Well, that was a kind of an "M" for model, but not the database kind. Let's take what we've learned and create a
database of movies.
P R E V IO U S NEXT
Add a model to an ASP.NET Core MVC app
9/18/2018 • 10 minutes to read • Edit Online
using System;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Scaffolding a controller
In Solution Explorer, right-click the Controllers folder > Add > New Scaffolded Item.
In the Add Scaffold dialog, tap MVC Controller with views, using Entity Framework > Add.
In Solution Explorer, right-click the Controllers folder > Add > Controller.
If the Add MVC Dependencies dialog appears:
Update Visual Studio to the latest version. Visual Studio versions prior to 15.5 show this dialog.
If you can't update, select ADD, and then follow the add controller steps again.
In the Add Scaffold dialog, tap MVC Controller with views, using Entity Framework > Add.
SqlException: Cannot open database "MvcMovieContext-<GUID removed>" requested by the login. The login failed.
Login failed for user 'Rick'.
You need to create the database, and you'll use the EF Core Migrations feature to do that. Migrations lets you
create a database that matches your data model and update the database schema when your data model changes.
Add-Migration Initial
Update-Database
Install-Package Microsoft.EntityFrameworkCore.Tools
Add-Migration Initial
Update-Database
Note: If you receive an error with the Install-Package command, open NuGet Package Manager and search for
the Microsoft.EntityFrameworkCore.Tools package. This allows you to install the package or check if it's already
installed. Alternatively, see the CLI approach if you have problems with the PMC.
The Add-Migration command creates code to create the initial database schema. The schema is based on the
model specified in the DbContext (In the Data/MvcMovieContext.cs file). The Initial argument is used to name
the migrations. You can use any name, but by convention you choose a name that describes the migration. See
Introduction to migrations for more information.
The Update-Database command runs the Up method in the Migrations/<time-stamp>_Initial.cs file, which creates
the database.
You can perform the preceeding steps using the command-line interface (CLI) rather than the PMC:
Add EF Core tooling to the .csproj file.
Run the following commands from the console (in the project directory):
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Create a couple more movie entries. Try the Edit, Details, and Delete links, which are all functional.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
The highlighted code above shows the movie database context being added to the Dependency Injection container
(In the Startup.cs file). services.AddDbContext<MvcMovieContext>(options => specifies the database to use and the
connection string. => is a lambda operator.
Open the Controllers/MoviesController.cs file and examine the constructor:
public class MoviesController : Controller
{
private readonly MvcMovieContext _context;
The constructor uses Dependency Injection to inject the database context ( MvcMovieContext ) into the controller.
The database context is used in each of the CRUD methods in the controller.
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The id parameter is generally passed as route data. For example http://localhost:5000/movies/details/1 sets:
The controller to the movies controller (the first URL segment).
The action to details (the second URL segment).
The id to 1 (the last URL segment).
You can also pass in the id with a query string as follows:
http://localhost:1234/movies/details?id=1
The id parameter is defined as a nullable type ( int? ) in case an ID value isn't provided.
A lambda expression is passed in to FirstOrDefaultAsync to select movie entities that match the route data or
query string value.
A lambda expression is passed in to SingleOrDefaultAsync to select movie entities that match the route data or
query string value.
If a movie is found, an instance of the Movie model is passed to the Details view:
return View(movie);
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Movie</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd>
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd>
@Html.DisplayFor(model => model.Genre)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd>
@Html.DisplayFor(model => model.Price)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
By including a @model statement at the top of the view file, you can specify the type of object that the view expects.
When you created the movie controller, Visual Studio automatically included the following @model statement at
the top of the Details.cshtml file:
@model MvcMovie.Models.Movie
This @model directive allows you to access the movie that the controller passed to the view by using a Model
object that's strongly typed. For example, in the Details.cshtml view, the code passes each movie field to the
DisplayNameFor and DisplayFor HTML Helpers with the strongly typed Model object. The Create and Edit
methods and views also pass a Movie model object.
Examine the Index.cshtml view and the Index method in the Movies controller. Notice how the code creates a
List object when it calls the View method. The code passes this Movies list from the Index action method to
the view:
// GET: Movies
public async Task<IActionResult> Index()
{
return View(await _context.Movie.ToListAsync());
}
When you created the movies controller, scaffolding automatically included the following @model statement at the
top of the Index.cshtml file:
@model IEnumerable<MvcMovie.Models.Movie>
The @modeldirective allows you to access the list of movies that the controller passed to the view by using a
Model object that's strongly typed. For example, in the Index.cshtml view, the code loops through the movies with
a foreach statement over the strongly typed Model object:
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Because the Model object is strongly typed (as an IEnumerable<Movie> object), each item in the loop is typed as
Movie . Among other benefits, this means that you get compile-time checking of the code:
Additional resources
Tag Helpers
Globalization and localization
P R E V IO U S A D D IN G A N E X T W O R K IN G W IT H
V IE W SQL
Work with SQL Server LocalDB in ASP.NET Core
9/18/2018 • 3 minutes to read • Edit Online
By Rick Anderson
The MvcMovieContext object handles the task of connecting to the database and mapping Movie objects to
database records. The database context is registered with the Dependency Injection container in the
ConfigureServices method in the Startup.cs file:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
The ASP.NET Core Configuration system reads the ConnectionString . For local development, it gets the
connection string from the appsettings.json file:
"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-
2;Trusted_Connection=True;MultipleActiveResultSets=true"
}
When you deploy the app to a test or production server, you can use an environment variable or another approach
to set the connection string to a real SQL Server. See Configuration for more information.
namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the DB, the seed initializer returns and no movies are added.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
using MvcMovie;
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
try
{
// Requires using MvcMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
P R E V IO U S NEXT
Controller methods and views in ASP.NET Core
9/18/2018 • 10 minutes to read • Edit Online
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal. We don't want to see the time (12:00:00
AM in the image below ) and ReleaseDate should be two words.
Open the Models/Movie.cs file and add the highlighted lines shown below:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
We cover DataAnnotations in the next tutorial. The Display attribute specifies what to display for the name of a
field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data
(Date), so the time information stored in the field isn't displayed.
The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly
map Price to currency in the database. For more information, see Data Types.
Browse to the Movies controller and hold the mouse pointer over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Core MVC Anchor Tag Helper in the
Views/Movies/Index.cshtml file.
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
code above, the AnchorTagHelper dynamically generates the HTML href attribute value from the controller action
method and route id. You use View Source from your favorite browser or use the developer tools to examine the
generated markup. A portion of the generated HTML is shown below:
<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
ASP.NET Core translates http://localhost:1234/Movies/Edit/4 into a request to the Edit action method of the
Movies controller with the parameter Id of 4. ( Controller methods are also known as action methods.)
Tag Helpers are one of the most popular new features in ASP.NET Core. See Additional resources for more
information.
Open the Movies controller and examine the two Edit action methods. The following code shows the
HTTP GET Edit method, which fetches the movie and populates the edit form generated by the Edit.cshtml Razor
file.
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [Bind] attribute is one way to protect against over-posting. You should only include properties in the [Bind]
attribute that you want to change. See Protect your controller from over-posting for more information.
ViewModels provide an alternative approach to prevent over-posting.
Notice the second Edit action method is preceded by the [HttpPost] attribute.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The HttpPost attribute specifies that this Edit method can be invoked only for POST requests. You could apply
the [HttpGet] attribute to the first edit method, but that's not necessary because [HttpGet] is the default.
The ValidateAntiForgeryToken attribute is used to prevent forgery of a request and is paired up with an anti-
forgery token generated in the edit view file (Views/Movies/Edit.cshtml). The edit view file generates the anti-
forgery token with the Form Tag Helper.
<form asp-action="Edit">
The Form Tag Helper generates a hidden anti-forgery token that must match the [ValidateAntiForgeryToken]
generated anti-forgery token in the Edit method of the Movies controller. For more information, see Anti-
Request Forgery.
The method takes the movie ID parameter, looks up the movie using the Entity Framework
HttpGet Edit
SingleOrDefaultAsync method, and returns the selected movie to the Edit view. If a movie cannot be found,
NotFound ( HTTP 404 ) is returned.
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
When the scaffolding system created the Edit view, it examined the Movie class and created code to render
<label> and <input> elements for each property of the class. The following example shows the Edit view that
was generated by the Visual Studio scaffolding system:
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<form asp-action="Edit">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Genre" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
<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.
The <input> elements are in an HTML <form> element whose action attribute is set to post to the
/Movies/Edit/id URL. The form data will be posted to the server when the Save button is clicked. The last line
before the closing </form> element shows the hidden XSRF token generated by the Form Tag Helper.
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [ValidateAntiForgeryToken] attribute validates the hidden XSRF token generated by the anti-forgery token
generator in the Form Tag Helper
The model binding system takes the posted form values and creates a Movie object that's passed as the movie
parameter. The ModelState.IsValid method verifies that the data submitted in the form can be used to modify
(edit or update) a Movie object. If the data is valid it's saved. The updated (edited) movie data is saved to the
database by calling the SaveChangesAsync method of database context. After saving the data, the code redirects the
user to the Index action method of the MoviesController class, which displays the movie collection, including the
changes just made.
Before the form is posted to the server, client side validation checks any validation rules on the fields. If there are
any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't
have client side validation but the server will detect the posted values that are not valid, and the form values will
be redisplayed with error messages. Later in the tutorial we examine Model Validation in more detail. The
Validation Tag Helper in the Views/Movies/Edit.cshtml view template takes care of displaying appropriate error
messages.
All the HttpGet methods in the movie controller follow a similar pattern. They get a movie object (or list of
objects, in the case of Index ), and pass the object (model) to the view. The Create method passes an empty
movie object to the Create view. All the methods that create, edit, delete, or otherwise modify data do so in the
[HttpPost] overload of the method. Modifying data in an HTTP GET method is a security risk. Modifying data in
an HTTP GET method also violates HTTP best practices and the architectural REST pattern, which specifies that
GET requests shouldn't change the state of your application. In other words, performing a GET operation should
be a safe operation that has no side effects and doesn't modify your persisted data.
Additional resources
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
Anti-Request Forgery
Protect your controller from over-posting
ViewModels
Form Tag Helper
Input Tag Helper
Label Tag Helper
Select Tag Helper
Validation Tag Helper
P R E V IO U S NEXT
Add search to an ASP.NET Core MVC app
6/26/2018 • 7 minutes to read • Edit Online
By Rick Anderson
In this section you add search capability to the Index action method that lets you search movies by genre or
name.
Update the Index method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the Index action method creates a LINQ query to select the movies:
The query is only defined at this point, it has not been run against the database.
If the searchString parameter contains a string, the movies query is modified to filter on the value of the search
string:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code above is a Lambda Expression. Lambdas are used in method-based LINQ
queries as arguments to standard query operator methods such as the Where method or Contains (used in the
code above). LINQ queries are not executed when they're defined or when they're modified by calling a method
such as Where , Contains or OrderBy . Rather, query execution is deferred. That means that the evaluation of an
expression is delayed until its realized value is actually iterated over or the ToListAsync method is called. For more
information about deferred query execution, see Query Execution.
Note: The Contains method is run on the database, not in the c# code shown above. The case sensitivity on the
query depends on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case
insensitive. In SQLlite, with the default collation, it's case sensitive.
Navigate to /Movies/Index . Append a query string such as ?searchString=Ghost to the URL. The filtered movies
are displayed.
If you change the signature of the Index method to have a parameter named id , the id parameter will match
the optional {id} placeholder for the default routes set in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
You can quickly rename the searchString parameter to id with the rename command. Right click on
searchString > Rename.
The rename targets are highlighted.
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}
You can now pass the search title as route data (a URL segment) instead of as a query string value.
However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add
UI elements to help them filter movies. If you changed the signature of the Index method to test how to pass the
route-bound ID parameter, change it back so that it takes a parameter named searchString :
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
Open the Views/Movies/Index.cshtml file, and add the <form> markup highlighted below:
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
The HTML <form> tag uses the Form Tag Helper, so when you submit the form, the filter string is posted to the
Index action of the movies controller. Save your changes and then test the filter.
There's no [HttpPost] overload of the Index method as you might expect. You don't need it, because the method
isn't changing the state of the app, just filtering data.
You could add the following [HttpPost] Index method.
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
The notUsed parameter is used to create an overload for the Index method. We'll talk about that later in the
tutorial.
If you add this method, the action invoker would match the [HttpPost] Index method, and the [HttpPost] Index
method would run as shown in the image below.
However, even if you add this [HttpPost] version of the Index method, there's a limitation in how this has all
been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends
that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request
is the same as the URL for the GET request (localhost:xxxxx/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. We'll fix this by specifying the request should be HTTP GET .
Notice how intelliSense helps us update the markup.
Notice the distinctive font in the <form> tag. That distinctive font indicates the tag is supported by Tag Helpers.
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.
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}
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.
The SelectList of genres is created by projecting the distinct genres (we don't want our select list to have
duplicate genres).
movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync())
@{
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.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.movies[0].Title)
In the preceding code, the DisplayNameFor HTML Helper inspects the Title property referenced in the lambda
expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you
don't receive an access violation when model , model.movies , or model.movies[0] are null or empty. When the
lambda expression is evaluated (for example, @Html.DisplayFor(modelItem => item.Title) ), the model's property
values are evaluated.
Test the app by searching by genre, by movie title, and by both.
P R E V IO U S NEXT
Add a new field to an ASP.NET Core MVC app
9/18/2018 • 4 minutes to read • Edit Online
By Rick Anderson
In this section you'll use Entity Framework Code First Migrations to add a new field to the model and migrate that
change to the database.
When you use EF Code First to automatically create a database, Code First adds a table to the database to help
track whether the schema of the database is in sync with the model classes it was generated from. If they aren't in
sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.
[Bind("ID,Title,ReleaseDate,Genre,Price,Rating")]
You also need to update the view templates in order to display, create and edit the new Rating property in the
browser view.
Edit the /Views/Movies/Index.cshtml file and add a Rating field:
<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>
Update the /Views/Movies/Create.cshtml with a Rating field. You can copy/paste the previous "form group" and
let intelliSense help you update the fields. IntelliSense works with Tag Helpers. Note: In the RTM verison of Visual
Studio 2017 you need to install the Razor Language Services for Razor intelliSense. This will be fixed in the next
release.
The app won't work until we update the DB to include the new field. If you run it now, you'll get the following
SqlException :
You're seeing this error because the updated Movie model class is different than the schema of the Movie table of
the existing database. (There's no Rating column in the database table.)
There are a few approaches to resolving the error:
1. Have the Entity Framework automatically drop and re-create the database based on the new model class
schema. This approach is very convenient early in the development cycle when you are 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.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of
this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, we'll use Code First Migrations.
Update the SeedData class so that it provides a value for the new column. A sample change is shown below, but
you'll want to make this change for each new Movie .
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},
Add-Migration Rating
Update-Database
The Add-Migration command tells the migration framework to examine the current Movie model with the current
Movie DB schema and create the necessary code to migrate the DB to the new model. The name "Rating" is
arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.
If you delete all the records in the DB, the initialize will seed the DB and include the Rating field. You can do this
with the delete links in the browser or from SSOX.
Run the app and verify you can create/edit/display movies with a Rating field. You should also add the Rating
field to the Edit , Details , and Delete view templates.
P R E V IO U S NEXT
Add validation to an ASP.NET Core MVC app
6/26/2018 • 10 minutes to read • Edit Online
By Rick Anderson
In this section you'll add validation logic to the Movie model, and you'll ensure that the validation rules are
enforced any time a user creates or edits a movie.
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
public class Movie
{
public int ID { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
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 indicates that a property must have a value; but nothing prevents a user
from entering white space to satisfy this validation. The RegularExpression attribute is used to limit what
characters can be input. In the code above, Genre and Rating must use only letters (First letter uppercase, white
space, numbers and special characters are not allowed). The Range attribute constrains a value to within a
specified range. The StringLength attribute lets you set the maximum length of a string property, and optionally
its minimum length. Value types (such as decimal , int , float , DateTime ) are inherently required and don't need
the [Required] attribute.
Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also
ensures that you can't forget to validate something and inadvertently let bad data into the database.
Notice how the form has automatically rendered an appropriate validation error message in each field containing
an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a
user has JavaScript disabled).
A significant benefit is that you didn't need to change a single line of code in the MoviesController class or in the
Create.cshtml view in order to enable this validation UI. The controller and views you created earlier in this tutorial
automatically picked up the validation rules that you specified by using validation attributes on the properties of
the Movie model class. Test validation using the Edit action method, and the same validation is applied.
The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a
break point in the HTTP Post method, by using the Fiddler tool , or the F12 Developer tools.
// GET: Movies/Create
public IActionResult Create()
{
return View();
}
// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}
The first (HTTP GET) Create action method displays the initial Create form. The second ( [HttpPost] ) version
handles the form post. The second Create method (The [HttpPost] version) calls ModelState.IsValid to check
whether the movie has any validation errors. Calling this method evaluates any validation attributes that have
been applied to the object. If the object has validation errors, the Create method re-displays the form. If there are
no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the
server when there are validation errors detected on the client side; the second Create method is never called
when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled
and you can test the HTTP POST Create method ModelState.IsValid detecting any validation errors.
You can set a break point in the [HttpPost] Create method and verify the method is never called, client side
validation won't submit the form data when validation errors are detected. If you disable JavaScript in your
browser, then submit the form with errors, the break point will be hit. You still get full validation without
JavaScript.
The following image shows how to disable JavaScript in the FireFox browser.
The following image shows how to disable JavaScript in the Chrome browser.
After you disable JavaScript, post invalid data and step through the debugger.
Below is portion of the Create.cshtml view template that you scaffolded earlier in the tutorial. It's used by the
action methods shown above both to display the initial form and to redisplay it in the event of an error.
<form asp-action="Create">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery
Validation on the client side. The Validation Tag Helper displays validation errors. See Validation for more
information.
What's really nice about this approach is that neither the controller nor the Create view template knows anything
about the actual validation rules being enforced or about the specific error messages displayed. The validation
rules and the error strings are specified only in the Movie class. These same validation rules are automatically
applied to the Edit view and any other views templates you might create that edit your model.
When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the
model (in this example, the Movie class). You won't have to worry about different parts of the application being
inconsistent with how the rules are enforced — all validation logic will be defined in one place and used
everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be
fully honoring the DRY principle.
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
The DataType attributes only provide hints for the view engine to format the data (and supplies
elements/attributes such as <a> for URL's and <a href="mailto:EmailAddress.com"> for email. You can use the
RegularExpression attribute to validate the format of the data. The DataType attribute is used to specify a data
type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want
to keep track of the date, not the time. The DataType Enumeration provides for many data types, such as Date,
Time, PhoneNumber, Currency, EmailAddress and more. The DataType attribute can also enable the application
to automatically provide type-specific features. For example, a mailto: link can be created for
DataType.EmailAddress , and a date selector can be provided for DataType.Date in browsers that support HTML5.
The DataType attributes emit HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers can
understand. The DataType attributes do not provide any validation.
DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed
according to the default formats based on the server's CultureInfo .
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should also be applied when the value is displayed
in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably
don't want the currency symbol in the text box for editing.)
You can use the DisplayFormat attribute by itself, but it's generally a good idea to use the DataType attribute. The
DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the
following benefits that you don't get with DisplayFormat:
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, etc.)
By default, the browser will render data using the correct format based on your locale.
The DataType attribute can enable MVC to choose the right field template to render the data (the
DisplayFormat if used by itself uses the string template).
NOTE
jQuery validation doesn't work with the Range attribute and DateTime . For example, the following code will always display
a client side validation error, even when the date is in the specified range:
You will need to disable jQuery date validation to use the Range attribute with DateTime . It's generally not a good
practice to compile hard dates in your models, so using the Range attribute and DateTime is discouraged.
The following code shows combining attributes on one line:
public class Movie
{
public int ID { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
In the next part of the series, we'll review the application and make some improvements to the automatically
generated Details and Delete methods.
Additional resources
Working with Forms
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
P R E V IO U S NEXT
Examine the Details and Delete methods of an
ASP.NET Core app
9/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
Open the Movie controller and examine the Details method:
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that
invokes the method. In this case it's a GET request with three URL segments, the Movies controller, the Details
method and an id value. Recall these segments are defined in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
EF makes it easy to search for data using the SingleOrDefaultAsync method. An important security feature built
into the method is that the code verifies that the search method has found a movie before it tries to do anything
with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from
http://localhost:xxxx/Movies/Details/1 to something like http://localhost:xxxx/Movies/Details/12345 (or some
other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an
exception.
Examine the Delete and DeleteConfirmed methods.
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
Note that the HTTP GET Delete method doesn't delete the specified movie, it returns a view of the movie where you
can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter,
performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.
The [HttpPost] method that deletes the data is named DeleteConfirmed to give the HTTP POST method a unique
signature or name. The two method signatures are shown below:
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
The common language runtime (CLR ) requires overloaded methods to have a unique parameter signature (same
method name but different list of parameters). However, here you need two Delete methods -- one for GET and
one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a
parameter.)
There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding
mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a
URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that
method. The solution is what you see in the example, which is to add the ActionName("Delete") attribute to the
DeleteConfirmed method. That attribute performs mapping for the routing system so that a URL that includes
/Delete/ for a POST request will find the DeleteConfirmed method.
Another common work around for methods that have identical names and signatures is to artificially change the
signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when
we added the notUsed parameter. You could do the same thing here for the [HttpPost] Delete method:
// POST: Movies/Delete/6
[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 app in Azure with SQL Database. The
instruction are for an ASP.NET app, not an ASP.NET Core app, but the steps are the same.
P R E V IO U S
Build web APIs with ASP.NET Core
8/22/2018 • 4 minutes to read • Edit Online
By Scott Addie
View or download sample code (how to download)
This document explains how to build a web API in ASP.NET Core and when it's most appropriate to use each
feature.
[HttpGet]
public async Task<ActionResult<List<Pet>>> GetAllAsync()
{
return await _repository.GetPetsAsync();
}
[HttpGet("{id}")]
[ProducesResponseType(404)]
public async Task<ActionResult<Pet>> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);
if (pet == null)
{
return NotFound();
}
return pet;
}
[HttpPost]
[ProducesResponseType(400)]
public async Task<ActionResult<Pet>> CreateAsync(Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddPetAsync(pet);
return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly PetsRepository _repository;
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Pet>), 200)]
public async Task<IActionResult> GetAllAsync()
{
var pets = await _repository.GetPetsAsync();
return Ok(pets);
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(Pet), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);
if (pet == null)
{
return NotFound();
}
return Ok(pet);
}
[HttpPost]
[ProducesResponseType(typeof(Pet), 201)]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddPetAsync(pet);
return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}
The ControllerBase class provides access to several properties and methods. In the preceding code, examples
include BadRequest(ModelStateDictionary) and CreatedAtAction(String, Object, Object). These methods are called
within action methods to return HTTP 400 and 201 status codes, respectively. The ModelState property, also
provided by ControllerBase , is accessed to handle request model validation.
A compatibility version of 2.1 or later, set via SetCompatibilityVersion, is required to use this attribute. For example,
the highlighted code in Startup.ConfigureServices sets the 2.1 compatibility flag:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
For more information, see Compatibility version for ASP.NET Core MVC.
The [ApiController] attribute is commonly coupled with ControllerBase to enable REST-specific behavior for
controllers. ControllerBase provides access to methods such as NotFound and File.
Another approach is to create a custom base controller class annotated with the [ApiController] attribute:
[ApiController]
public class MyBaseController
{
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
The default behavior is disabled when the SuppressModelStateInvalidFilter property is set to true . Add the
following code in Startup.ConfigureServices after
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
WARNING
Don't use [FromRoute] when values might contain %2f (that is / ). %2f won't be unescaped to / . Use [FromQuery] if
the value might contain %2f .
Without the [ApiController] attribute, binding source attributes are explicitly defined. In the following example,
the [FromQuery] attribute indicates that the discontinuedOnly parameter value is provided in the request URL's
query string:
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAsync(
[FromQuery] bool discontinuedOnly = false)
{
List<Product> products = null;
if (discontinuedOnly)
{
products = await _repository.GetDiscontinuedProductsAsync();
}
else
{
products = await _repository.GetProductsAsync();
}
return products;
}
Inference rules are applied for the default data sources of action parameters. These rules configure the binding
sources you're otherwise likely to manually apply to the action parameters. The binding source attributes behave as
follows:
[FromBody] is inferred for complex type parameters. An exception to this rule is any complex, built-in type with
a special meaning, such as IFormCollection and CancellationToken. The binding source inference code ignores
those special types. [FromBody] isn't inferred for simple types such as string or int . Therefore, the
[FromBody] attribute should be used for simple types when that functionality is desired. When an action has
more than one parameter explicitly specified (via [FromBody] ) or inferred as bound from the request body, an
exception is thrown. For example, the following action signatures cause an exception:
// Don't do this. All of the following actions result in an exception.
[HttpPost]
public IActionResult Action1(Product product,
Order order) => null;
[HttpPost]
public IActionResult Action2(Product product,
[FromBody] Order order) => null;
[HttpPost]
public IActionResult Action3([FromBody] Product product,
[FromBody] Order order) => null;
[FromForm ] is inferred for action parameters of type IFormFile and IFormFileCollection. It's not inferred for
any simple or user-defined types.
[FromRoute] is inferred for any action parameter name matching a parameter in the route template. When
more than one route matches an action parameter, any route value is considered [FromRoute] .
[FromQuery] is inferred for any other action parameters.
The default inference rules are disabled when the SuppressInferBindingSourcesForParameters property is set to
true . Add the following code in Startup.ConfigureServices after
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
Overview
This tutorial creates the following API:
GET /api/todo Get all to-do items None Array of to-do items
The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a client.
Postman or curl is used as the client to test the app.
A model is an object that represents the data in the app. In this case, the only model is a to-do item. Models
are represented as C# classes, also known as Plain Old CLR Object (POCOs).
A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single
controller.
To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items in
an in-memory database.
Prerequisites
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code
The TodoApi folder opens in Visual Studio Code (VS Code). Select the Startup.cs file.
Select Yes to the Warn message "Required assets to build and debug are missing from 'TodoApi'. Add them?"
Select Restore to the Info message "There are unresolved dependencies".
Press Debug (F5) to build and run the program. In a browser, navigate to http://localhost:5000/api/values. The
following output is displayed:
["value1","value2"]
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>
Creating a new project in ASP.NET Core 2.0 adds the Microsoft.AspNetCore.All package reference to the
TodoApi.csproj file:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
There's no need to install the Entity Framework Core InMemory database provider separately. This database
provider allows Entity Framework Core to be used with an in-memory database.
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}
Add a controller
In the Controllers folder, create a class named TodoController . Replace its contents with the following code:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
The preceding code defines an API controller class without methods. In the next sections, methods are added to
implement the API.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
In the next sections, methods are added to implement the API. The class is annotated with an [ApiController]
attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate
class with ApiControllerAttribute.
The controller's constructor uses Dependency Injection to inject the database context ( TodoContext ) into the
controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an
item to the in-memory database if one doesn't exist.
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each method
is constructed as follows:
Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which is the controller class name minus the "Controller"
suffix. For this sample, the controller class name is TodoController and the root name is "todo". ASP.NET Core
routing is case insensitive.
If the [HttpGet] attribute has a route template (such as [HttpGet("/products")] , append that to the path. This
sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.
If no item matches the requested ID, the method returns a 404 error. Returning NotFound returns an HTTP 404
response.
Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200
response.
Launch the app
In VS Code, press F5 to launch the app. Navigate to http://localhost:5000/api/todo (the Todo controller we
created).
Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following
markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following code:
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ 'display': 'block' });
}
$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open
launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to
open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a
complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the
experience richer. Below are explanations around the calls to the API.
Get a list of to -do items
To get a list of to-do items, send an HTTP GET request to /api/todo.
The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array.
This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url . GET is used
as the type . The success callback function is invoked if the request succeeds. In the callback, the DOM is updated
with the to-do information.
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
Add a to -do item
To add a to-do item, send an HTTP POST request to /api/todo/. The request body should contain a to-do object.
The ajax function is using POST to call the API. For POST and PUT requests, the request body represents the data
sent to the API. The API is expecting a JSON request body. The accepts and contentType options are set to
application/json to classify the media type being received and sent, respectively. The data is converted to a JSON
object using JSON.stringify . When the API returns a successful status code, the getData function is invoked to
update the HTML table.
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
Implement the other CRUD operations
In the following sections, Create , Update , and Delete methods are added to the controller.
Create
Add the following Create method:
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. The [FromBody] attribute
tells MVC to get the value of the to-do item from the body of the HTTP request.
[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. MVC gets the value of the
to-do item from the body of the HTTP request.
The CreatedAtRoute method:
Returns a 201 response. 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.
See 10.2.2 201 Created.
Uses the "GetTodo" named route to create the URL. The "GetTodo" named route is defined in GetById :
TIP
If no response displays after clicking Send, disable the SSL certification verification option. This is found under File >
Settings. Click the Send button again after disabling the setting.
Click the Headers tab in the Response pane and copy the Location header value:
The Location header URI can be used to access the new item.
Update
Add the following Update method:
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
Update is similar to Create , 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 deltas. To support
partial updates, use HTTP PATCH.
Use Postman to update the to-do item's name to "walk cat":
Delete
Add the following Delete method:
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
Next steps
For information on using a persistent database, see:
Create a Razor Pages web app with ASP.NET Core
Work with data in ASP.NET Core
ASP.NET Core Web API help pages using Swagger
Routing to controller actions
Build web APIs with ASP.NET Core
Controller action return types
For information about deploying an API, including to Azure App Service, see Host and deploy.
View or download sample code. See how to download.
Create a Web API with ASP.NET Core and Visual
Studio for Mac
9/18/2018 • 16 minutes to read • Edit Online
Overview
This tutorial creates the following API:
GET /api/todo Get all to-do items None Array of to-do items
The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a client.
Postman or curl is used as the client to test the app.
A model is an object that represents the data in the app. In this case, the only model is a to-do item. Models
are represented as C# classes, also known as Plain Old CLR Object (POCOs).
A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single
controller.
To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items in
an in-memory database.
See Introduction to ASP.NET Core MVC on macOS or Linux for an example that uses a persistent database.
Prerequisites
Visual Studio for Mac
Select .NET Core App > ASP.NET Core Web API > Next.
Enter TodoApi for the Project Name, and then click Create.
NOTE
You can put model classes anywhere in your project, but the Models folder is used by convention.
Right-click the Models folder, and select Add > New File > General > Empty Class. Name the class TodoItem,
and then click New.
Replace the generated code with:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}
Add a controller
In Solution Explorer, in the Controllers folder, add the class TodoController .
Replace the generated code with the following:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
The preceding code defines an API controller class without methods. In the next sections, methods are added to
implement the API.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
In the next sections, methods are added to implement the API. The class is annotated with an [ApiController]
attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate
class with ApiControllerAttribute.
The controller's constructor uses Dependency Injection to inject the database context ( TodoContext ) into the
controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an
item to the in-memory database if one doesn't exist.
[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each method
is constructed as follows:
Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which is the controller class name minus the "Controller"
suffix. For this sample, the controller class name is TodoController and the root name is "todo". ASP.NET Core
routing is case insensitive.
If the [HttpGet] attribute has a route template (such as [HttpGet("/products")] , append that to the path. This
sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.
If no item matches the requested ID, the method returns a 404 error. Returning NotFound returns an HTTP 404
response.
Otherwise, the method returns 200 with a JSON response body. Returning item results in an HTTP 200
response.
Launch the app
In Visual Studio, select Run > Start With Debugging to launch the app. Visual Studio launches a browser and
navigates to http://localhost:<port> , where <port> is a randomly chosen port number. You get an HTTP 404
(Not Found) error. Change the URL to http://localhost:<port>/api/values . The ValuesController data is
displayed:
["value1","value2"]
[{"key":1,"name":"Item1","isComplete":false}]
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding method responds to an HTTP POST, as indicated by the [HttpPost] attribute. MVC gets the value of
the to-do item from the body of the HTTP request.
The CreatedAtRoute method returns a 201 response. It's the standard response for an HTTP POST method that
creates a new resource on the server. CreatedAtRoute also adds a Location header to the response. The Location
header specifies the URI of the newly created to-do item. See 10.2.2 201 Created.
Use Postman to send a Create request
Start the app (Run > Start With Debugging).
Open Postman.
{
"name":"walk dog",
"isComplete":true
}
TIP
If no response displays after clicking Send, disable the SSL certification verification option. This is found under File >
Settings. Click the Send button again after disabling the setting.
Click the Headers tab in the Response pane and copy the Location header value:
You can use the Location header URI to access the resource you created. The Create method returns
CreatedAtRoute. The first parameter passed to CreatedAtRoute represents the named route to use for generating
the URL. Recall that the GetById method created the "GetTodo" named route:
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
Update is similar to Create , but uses HTTP PUT. The response is 204 (No Content). According to the HTTP spec, a
PUT request requires the client to send the entire updated entity, not just the deltas. To support partial updates, use
HTTP PATCH.
{
"key": 1,
"name": "walk dog",
"isComplete": true
}
Delete
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following
markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following code:
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ 'display': 'block' });
}
$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open
launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to
open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a
complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the
experience richer. Below are explanations around the calls to the API.
Get a list of to -do items
To get a list of to-do items, send an HTTP GET request to /api/todo.
The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array.
This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url . GET is used
as the type . The success callback function is invoked if the request succeeds. In the callback, the DOM is updated
with the to-do information.
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
Next steps
For information on using a persistent database, see:
Create a Razor Pages web app with ASP.NET Core
Work with data in ASP.NET Core
ASP.NET Core Web API help pages using Swagger
Routing to controller actions
Build web APIs with ASP.NET Core
Controller action return types
For information about deploying an API, including to Azure App Service, see Host and deploy.
View or download sample code. See how to download.
Create a Web API with ASP.NET Core and Visual
Studio
8/9/2018 • 16 minutes to read • Edit Online
Overview
This tutorial creates the following API:
GET /api/todo Get all to-do items None Array of to-do items
The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a
client. Postman or curl is used as the client to test the app.
A model is an object that represents the data in the app. In this case, the only model is a to-do item.
Models are represented as C# classes, also known as Plain Old CLR Object (POCOs).
A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single
controller.
To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items
in an in-memory database.
Prerequisites
Visual Studio 2017 version 15.7.3 or later with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
["value1","value2"]
NOTE
The model classes can go anywhere in the project. The Models folder is used by convention for model classes.
In Solution Explorer, right-click the Models folder and select Add > Class. Name the class TodoItem and click
Add.
Update the TodoItem class with the following code:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
The preceding code defines an API controller class without methods. In the next sections, methods are added to
implement the API.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
In the next sections, methods are added to implement the API. The class is annotated with an [ApiController]
attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate
class with ApiControllerAttribute.
The controller's constructor uses Dependency Injection to inject the database context ( TodoContext ) into the
controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an
item to the in-memory database if one doesn't exist.
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each
method is constructed as follows:
Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which is the controller class name minus the
"Controller" suffix. For this sample, the controller class name is TodoController and the root name is "todo".
ASP.NET Core routing is case insensitive.
If the [HttpGet] attribute has a route template (such as [HttpGet("/products")] , append that to the path. This
sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. The [FromBody] attribute
tells MVC to get the value of the to-do item from the body of the HTTP request.
[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. MVC gets the value of the
to-do item from the body of the HTTP request.
The CreatedAtRoute method:
Returns a 201 response. 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. See 10.2.2 201 Created.
Uses the "GetTodo" named route to create the URL. The "GetTodo" named route is defined in GetById :
{
"name":"walk dog",
"isComplete":true
}
TIP
If no response displays after clicking Send, disable the SSL certification verification option. This is found under File >
Settings. Click the Send button again after disabling the setting.
Click the Headers tab in the Response pane and copy the Location header value:
The Location header URI can be used to access the new item.
Update
Add the following Update method:
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
Update is similar to Create , 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 deltas. To support
partial updates, use HTTP PATCH.
Use Postman to update the to-do item's name to "walk cat":
Delete
Add the following Delete method:
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following
markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following
code:
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ 'display': 'block' });
}
$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open
launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to
open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a
complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the
experience richer. Below are explanations around the calls to the API.
Get a list of to -do items
To get a list of to-do items, send an HTTP GET request to /api/todo.
The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array.
This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url . GET is
used as the type . The success callback function is invoked if the request succeeds. In the callback, the DOM is
updated with the to-do information.
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
Next steps
For information on using a persistent database, see:
Create a Razor Pages web app with ASP.NET Core
Work with data in ASP.NET Core
ASP.NET Core Web API help pages using Swagger
Routing to controller actions
Build web APIs with ASP.NET Core
Controller action return types
For information about deploying an API, including to Azure App Service, see Host and deploy.
View or download sample code. See how to download.
Create backend services for native mobile apps with
ASP.NET Core
9/26/2018 • 8 minutes to read • Edit Online
By Steve Smith
Mobile apps can easily communicate with ASP.NET Core backend services.
View or download sample backend services code
Features
The ToDoRest app supports listing, adding, deleting, and updating To-Do items. Each item has an ID, a Name,
Notes, and a property indicating whether it's been Done yet.
The main view of the items, as shown above, lists each item's name and indicates if it's done with a checkmark.
Tapping the + icon opens an add item dialog:
Tapping an item on the main list screen opens up an edit dialog where the item's Name, Notes, and Done settings
can be modified, or the item can be deleted:
This sample is configured by default to use backend services hosted at developer.xamarin.com, which allow read-
only operations. To test it out yourself against the ASP.NET Core app created in the next section running on your
computer, you'll need to update the app's RestUrl constant. Navigate to the ToDoREST project and open the
Constants.cs file. Replace the RestUrl with a URL that includes your machine's IP address (not localhost or
127.0.0.1, since this address is used from the device emulator, not from your machine). Include the port number as
well (5000). In order to test that your services work with a device, ensure you don't have an active firewall blocking
access to this port.
NOTE
Make sure you run the application directly, rather than behind IIS Express, which ignores non-local requests by default. Run
dotnet run from a command prompt, or choose the application name profile from the Debug Target dropdown in the Visual
Studio toolbar.
Add a model class to represent To-Do items. Mark required fields using the [Required] attribute:
using System.ComponentModel.DataAnnotations;
namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Notes { get; set; }
The API methods require some way to work with data. Use the same IToDoRepository interface the original
Xamarin sample uses:
using System.Collections.Generic;
using ToDoApi.Models;
namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}
For this sample, the implementation just uses a private collection of items:
using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;
public ToDoRepository()
{
InitializeData();
}
_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}
services.AddSingleton<IToDoRepository,ToDoRepository>();
}
TIP
Learn more about creating web APIs in Build your first Web API with ASP.NET Core MVC and Visual Studio.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;
This API supports four different HTTP verbs to perform CRUD (Create, Read, Update, Delete) operations on the
data source. The simplest of these is the Read operation, which corresponds to an HTTP GET request.
Reading Items
Requesting a list of items is done with a GET request to the List method. The [HttpGet] attribute on the List
method indicates that this action should only handle GET requests. The route for this action is the route specified
on the controller. You don't necessarily need to use the action name as part of the route. You just need to ensure
each action has a unique and unambiguous route. Routing attributes can be applied at both the controller and
method levels to build up specific routes.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}
The List method returns a 200 OK response code and all of the ToDo items, serialized as JSON.
You can test your new API method using a variety of tools, such as Postman, shown here:
Creating Items
By convention, creating new data items is mapped to the HTTP POST verb. The Create method has an
[HttpPost] attribute applied to it, and accepts a ToDoItem instance. Since the item argument will be passed in the
body of the POST, this parameter is decorated with the [FromBody] attribute.
Inside the method, the item is checked for validity and prior existence in the data store, and if no issues occur, it's
added using the repository. Checking ModelState.IsValid performs model validation, and should be done in every
API method that accepts user input.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}
The sample uses an enum containing error codes that are passed to the mobile client:
Test adding new items using Postman by choosing the POST verb providing the new object in JSON format in the
Body of the request. You should also add a request header specifying a Content-Type of application/json .
The method returns the newly created item in the response.
Updating Items
Modifying records is done using HTTP PUT requests. Other than this change, the Edit method is almost identical
to Create . Note that if the record isn't found, the Edit action will return a NotFound (404) response.
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}
To test with Postman, change the verb to PUT. Specify the updated object data in the Body of the request.
This method returns a NoContent (204) response when successful, for consistency with the pre-existing API.
Deleting Items
Deleting records is accomplished by making DELETE requests to the service, and passing the ID of the item to be
deleted. As with updates, requests for items that don't exist will receive NotFound responses. Otherwise, a
successful request will get a NoContent (204) response.
[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}
Note that when testing the delete functionality, nothing is required in the Body of the request.
Additional resources
Authentication and Authorization
ASP.NET Core Web API help pages with Swagger /
OpenAPI
9/21/2018 • 2 minutes to read • Edit Online
Each public action method in your controllers can be tested from the UI. Click a method name to expand the
section. Add any necessary parameters, and click Try it out!.
NOTE
The Swagger UI version used for the screenshots is version 2. For a version 3 example, see Petstore example.
Next steps
Get started with Swashbuckle
Get started with NSwag
Get started with NSwag and ASP.NET Core
9/24/2018 • 6 minutes to read • Edit Online
Features
The main reason to use NSwag is the ability to not only introduce the Swagger UI and Swagger generator, but to
also make use of the flexible code generation capabilities. You don't need an existing API—you can use third-party
APIs that incorporate Swagger and let NSwag generate a client implementation. Either way, the development cycle
is expedited and you can more easily adapt to API changes.
Package installation
The NSwag NuGet package can be added with the following approaches:
Visual Studio
Visual Studio for Mac
Visual Studio Code
.NET Core CLI
From the Package Manager Console window:
Go to View > Other Windows > Package Manager Console
Navigate to the directory in which the TodoApi.csproj file exists
Execute the following command:
Install-Package NSwag.AspNetCore
using NJsonSchema;
using NSwag.AspNetCore;
using System.Reflection;
In the Startup.Configure method, enable the middleware for serving the generated Swagger specification and the
Swagger UI v3:
app.UseMvc();
}
Launch the app. Navigate to http://localhost:<port>/swagger to view the Swagger UI. Navigate to
http://localhost:<port>/swagger/v1/swagger.json to view the Swagger specification.
Code generation
Via NSwagStudio
Install NSwagStudio from the official GitHub repository.
Launch NSwagStudio. Enter the swagger.json file URL in the Swagger Specification URL textbox, and click the
Create local Copy button.
Select the CSharp Client client output type. Other options include TypeScript Client and CSharp Web API
Controller. Using a Web API Controller is basically a reverse generation. It uses a specification of a service to
rebuild the service.
Click the Generate Outputs button. A complete C# client implementation of the TodoApi.NSwag project is
produced. Click the CSharp Client tab of the Outputs section to see the generated client code:
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v11.17.3.0 (NJsonSchema v9.10.46.0 (Newtonsoft.Json v9.0.0.0))
(http://NSwag.org)
// </auto-generated>
//----------------------
namespace MyNamespace
{
#pragma warning disable // Disable all warnings
[System.CodeDom.Compiler.GeneratedCode("NSwag",
"11.17.3.0 (NJsonSchema v9.10.46.0 (Newtonsoft.Json v9.0.0.0))")]
public partial class TodoClient
{
private string _baseUrl = "http://localhost:50499";
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
public TodoClient()
{
_settings = new System.Lazy
<Newtonsoft.Json.JsonSerializerSettings>(() =>
{
var settings = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
});
}
TIP
The C# client code is generated based on settings defined in the Settings tab of the CSharp Client tab. Modify the settings
to perform tasks such as default namespace renaming and synchronous method generation.
Copy the generated C# code into a file in a client project (for example, a Xamarin.Forms app).
Start consuming the web API:
Customize
Swagger provides options for documenting the object model to ease consumption of the web API.
API info and description
In the Startup.Configure method, a configuration action passed to the UseSwagger method adds information such
as the author, license, and description:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding action returns IActionResult , but inside the action it's returning either CreatedAtRoute or
BadRequest. Data annotations are used to tell clients which HTTP status codes this action is known to return.
Decorate the action with the following attributes:
[ProducesResponseType(typeof(TodoItem), 201)] // Created
[ProducesResponseType(400)] // BadRequest
NSwag uses Reflection, and the recommended return type for web API actions is ActionResult<T>. Consequently,
NSwag can only infer the return type defined by T . Other possible return types in the action cannot be inferred.
Consider the following example:
[HttpPost]
public ActionResult<TodoItem> Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding action returns ActionResult<T> , but inside the action it's returning either CreatedAtRoute. Since the
controller is decorated with the [ApiController] attribute, a BadRequest response is possible too. See Automatic
HTTP 400 responses for more info. Data annotations are used to tell clients which HTTP status codes this action is
known to return. Decorate the action with the following attributes:
[ProducesResponseType(201)] // Created
[ProducesResponseType(400)] // BadRequest
The Swagger generator can now accurately describe this action, and generated clients know what they receive
when calling the endpoint. Decorating all actions with these attributes is highly recommended. For guidelines on
what HTTP responses your API actions should return, see the RFC 7231 specification.
Get started with Swashbuckle and ASP.NET Core
8/20/2018 • 11 minutes to read • Edit Online
Package installation
Swashbuckle can be added with the following approaches:
Visual Studio
Visual Studio for Mac
Visual Studio Code
.NET Core CLI
From the Package Manager Console window:
Go to View > Other Windows > Package Manager Console
Navigate to the directory in which the TodoApi.csproj file exists
Execute the following command:
Install-Package Swashbuckle.AspNetCore
using Swashbuckle.AspNetCore.Swagger;
In the Startup.Configure method, enable the middleware for serving the generated JSON document and the
Swagger UI:
app.UseMvc();
}
The preceding UseSwaggerUI method call enables the Static Files Middleware. If targeting .NET Framework or
.NET Core 1.x, add the Microsoft.AspNetCore.StaticFiles NuGet package to the project.
Launch the app, and navigate to http://localhost:<port>/swagger/v1/swagger.json . The generated document
describing the endpoints appears as shown in Swagger specification (swagger.json).
The Swagger UI can be found at http://localhost:<port>/swagger . Explore the API via Swagger UI and
incorporate it in other programs.
TIP
To serve the Swagger UI at the app's root ( http://localhost:<port>/ ), set the RoutePrefix property to an empty
string:
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = string.Empty;
});
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
warning CS1591: Missing XML comment for publicly visible type or member 'TodoController.GetAll()'
To suppress warnings project-wide, define a semicolon-delimited list of warning codes to ignore in the project file.
Appending the warning codes to $(NoWarn); applies the C# default values too.
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
To suppress warnings only for specific members, enclose the code in #pragma warning preprocessor directives.
This approach is useful for code that shouldn't be exposed via the API docs. In the following example, warning
code CS1591 is ignored for the entire Program class. Enforcement of the warning code is restored at the close of
the class definition. Specify multiple warning codes with a comma-delimited list.
namespace TodoApi
{
#pragma warning disable CS1591
public class Program
{
public static void Main(string[] args) =>
BuildWebHost(args).Run();
Configure Swagger to use the generated XML file. For Linux or non-Windows operating systems, file names and
paths can be case-sensitive. For example, a TodoApi.XML file is valid on Windows but not CentOS.
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetEntryAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
}
In the preceding code, Reflection is used to build an XML file name matching that of the Web API project. The
AppContext.BaseDirectory property is used to construct a path to the XML file.
Adding triple-slash comments to an action enhances the Swagger UI by adding the description to the section
header. Add a <summary> element above the Delete action:
/// <summary>
/// Deletes a specific TodoItem.
/// </summary>
/// <param name="id"></param>
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
The Swagger UI displays the inner text of the preceding code's <summary> element:
The UI is driven by the generated JSON schema:
"delete": {
"tags": [
"Todo"
],
"summary": "Deletes a specific TodoItem.",
"operationId": "ApiTodoByIdDelete",
"consumes": [],
"produces": [],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
Add a <remarks> element to the Create action method documentation. It supplements information specified in
the <summary> element and provides a more robust Swagger UI. The <remarks> element content can consist of
text, JSON, or XML.
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(typeof(TodoItem), 201)]
[ProducesResponseType(400)]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public ActionResult<TodoItem> Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
[Required]
public string Name { get; set; }
[DefaultValue(false)]
public bool IsComplete { get; set; }
}
}
The presence of this attribute changes the UI behavior and alters the underlying JSON schema:
"definitions": {
"TodoItem": {
"required": [
"name"
],
"type": "object",
"properties": {
"id": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"isComplete": {
"default": false,
"type": "boolean"
}
}
}
},
Add the [Produces("application/json")] attribute to the API controller. Its purpose is to declare that the
controller's actions support a response content type of application/json:
[Produces("application/json")]
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
The Response Content Type drop-down selects this content type as the default for the controller's GET actions:
As the usage of data annotations in the Web API increases, the UI and API help pages become more descriptive
and useful.
Describe response types
Consuming developers are most concerned with what's returned—specifically response types and error codes (if
not standard). The response types and error codes are denoted in the XML comments and data annotations.
The Create action returns an HTTP 201 status code on success. An HTTP 400 status code is returned when the
posted request body is null. Without proper documentation in the Swagger UI, the consumer lacks knowledge of
these expected outcomes. Fix that problem by adding the highlighted lines in the following example:
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(typeof(TodoItem), 201)]
[ProducesResponseType(400)]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
/// POST /Todo
/// {
/// "id": 1,
/// "name": "Item1",
/// "isComplete": true
/// }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public ActionResult<TodoItem> Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The Swagger UI now clearly documents the expected HTTP response codes:
Customize the UI
The stock UI is both functional and presentable. However, API documentation pages should represent your brand
or theme. Branding the Swashbuckle components requires adding the resources to serve static files and building
the folder structure to host those files.
If targeting .NET Framework or .NET Core 1.x, add the Microsoft.AspNetCore.StaticFiles NuGet package to the
project:
The preceding NuGet package is already installed if targeting .NET Core 2.x and using the metapackage.
Enable the static files middleware:
app.UseMvc();
}
Acquire the contents of the dist folder from the Swagger UI GitHub repository. This folder contains the necessary
assets for the Swagger UI page.
Create a wwwroot/swagger/ui folder, and copy into it the contents of the dist folder.
Create a custom.css file, in wwwroot/swagger/ui, with the following CSS to customize the page header:
.swagger-ui .topbar {
background-color: #000;
border-bottom: 3px solid #547f00;
}
Reference custom.css in the index.html file, after any other CSS files:
<link href="https://fonts.googleapis.com/css?
family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./swagger-ui.css">
<link rel="stylesheet" type="text/css" href="custom.css">
Get started with Razor Pages and Entity Framework Core using Visual Studio
Get started with Razor Pages and EF
Create, Read, Update, and Delete operations
Sort, filter, page, and group
Migrations
Create a complex data model
Read related data
Update related data
Handle concurrency conflicts
Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
Get started
Create, Read, Update, and Delete operations
Sort, filter, page, and group
Migrations
Create a complex data model
Read related data
Update related data
Handle concurrency conflicts
Inheritance
Advanced topics
ASP.NET Core with EF Core - new database (Entity Framework Core documentation site)
ASP.NET Core with EF Core - existing database (Entity Framework Core documentation site)
Get started with ASP.NET Core and Entity Framework 6
Azure Storage
Add Azure Storage by using Visual Studio Connected Services
Get started with Azure Blob storage and Visual Studio Connected Services
Get started with Queue Storage and Visual Studio Connected Services
Get started with Azure Table Storage and Visual Studio Connected Services
ASP.NET Core Razor Pages with EF Core - tutorial
series
7/10/2018 • 2 minutes to read • Edit Online
This series of tutorials teaches how to create ASP.NET Core Razor Pages web apps that use Entity Framework (EF )
Core for data access.
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
Razor Pages with Entity Framework Core in ASP.NET
Core - Tutorial 1 of 8
9/18/2018 • 15 minutes to read • Edit Online
The ASP.NET Core 2.0 version of this tutorial can be found in this PDF file.
The ASP.NET Core 2.1 version of this tutorial has many improvements over the 2.0 version.
By Tom Dykstra and Rick Anderson
The Contoso University sample web app demonstrates how to create an ASP.NET Core Razor Pages app using
Entity Framework (EF ) Core.
The sample app is a web site for a fictional Contoso University. It includes functionality such as student admission,
course creation, and instructor assignments. This page is the first in a series of tutorials that explain how to build
the Contoso University sample app.
Download or view the completed app. Download instructions.
Prerequisites
Visual Studio
.NET Core CLI
Visual Studio 2017 version 15.7.3 or later with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
Familiarity with Razor Pages. New programmers should complete Get started with Razor Pages before starting this
series.
Troubleshooting
If you run into a problem you can't resolve, you can generally find the solution by comparing your code to the
completed project. A good way to get help is by posting a question to StackOverflow.com for ASP.NET Core or EF
Core.
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Students/Index">Students</a></li>
<li><a asp-page="/Courses/Index">Courses</a></li>
<li><a asp-page="/Instructors/Index">Instructors</a></li>
<li><a asp-page="/Departments/Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
In Pages/Index.cshtml, replace the contents of the file with the following code to replace the text about ASP.NET
and MVC with text about this app:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core Razor Pages web app.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of tutorials.</p>
<p>
<a class="btn btn-default"
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro">
See the tutorial »
</a>
</p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p>
<a class="btn btn-default"
href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-rp/intro/samples/cu-final">
See project source code »
</a>
</p>
</div>
</div>
There's a one-to-many relationship between Student and Enrollment entities. There's a one-to-many relationship
between Course and Enrollment entities. A student can enroll in any number of courses. A course can have any
number of students enrolled in it.
In the following sections, a class for each one of these entities is created.
The Student entity
Create a Models folder. In the Models folder, create a class file named Student.cs with the following code:
using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
The ID property becomes the primary key column of the database (DB ) table that corresponds to this class. By
default, EF Core interprets a property that's named ID or classnameID as the primary key. In classnameID ,
classname is the name of the class. The alternative automatically recognized primary key is StudentID in the
preceding example.
The Enrollments property is a navigation property. Navigation properties link to other entities that are related to
this entity. In this case, the Enrollments property of a Student entity holds all of the Enrollment entities that are
related to that Student . For example, if a Student row in the DB has two related Enrollment rows, the Enrollments
navigation property contains those two Enrollment entities. A related Enrollment row is a row that contains that
student's primary key value in the StudentID column. For example, suppose the student with ID=1 has two rows in
the Enrollment table. The Enrollment table has two rows with StudentID = 1. StudentID is a foreign key in the
Enrollment table that specifies the student in the Student table.
If a navigation property can hold multiple entities, the navigation property must be a list type, such as
ICollection<T> . ICollection<T> can be specified, or a type such as List<T> or HashSet<T> . When ICollection<T>
is used, EF Core creates a HashSet<T> collection by default. Navigation properties that hold multiple entities come
from many-to-many and one-to-many relationships.
The Enrollment entity
The EnrollmentIDproperty is the primary key. This entity uses the classnameID pattern instead of ID like the
Student entity. Typically developers choose one pattern and use it throughout the data model. In a later tutorial,
using ID without classname is shown to make it easier to implement inheritance in the data model.
The Grade property is an enum . The question mark after the Grade type declaration indicates that the Grade
property is nullable. A grade that's null is different from a zero grade -- null means a grade isn't known or hasn't
been assigned yet.
The StudentID property is a foreign key, and the corresponding navigation property is Student . An Enrollment
entity is associated with one Student entity, so the property contains a single Student entity. The Student entity
differs from the Student.Enrollments navigation property, which contains multiple Enrollment entities.
The CourseID property is a foreign key, and the corresponding navigation property is Course . An Enrollment
entity is associated with one Course entity.
EF Core interprets a property as a foreign key if it's named <navigation property name><primary key property name> .
For example, StudentID for the Student navigation property, since the Student entity's primary key is ID .
Foreign key properties can also be named <primary key property name> . For example, CourseID since the Course
entity's primary key is CourseID .
The Course entity
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
The Enrollments property is a navigation property. A Course entity can be related to any number of Enrollment
entities.
The DatabaseGenerated attribute allows the app to specify the primary key rather than having the DB generate it.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}
The name of the connection string is passed in to the context by calling a method on a DbContextOptions object.
For local development, the ASP.NET Core configuration system reads the connection string from the
appsettings.json file.
Update main
In Program.cs, modify the Main method to do the following:
Get a DB context instance from the dependency injection container.
Call the EnsureCreated.
Dispose the context when the EnsureCreated method completes.
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
EnsureCreated ensures that the database for the context exists. If it exists, no action is taken. If it does not exist, then
the database and all its schema are created. EnsureCreated does not use migrations to create the database. A
database that is created with EnsureCreated cannot be later updated using migrations.
EnsureCreated is called on app start, which allows the following work flow:
Delete the DB.
Change the DB schema (for example, add an EmailAddress field).
Run the app.
EnsureCreated creates a DB with the EmailAddress column.
EnsureCreated is convenient early in development when the schema is rapidly evolving. Later in the tutorial the DB
is deleted and migrations are used.
Test the app
Run the app and accept the cookie policy. This app doesn't keep personal information. You can read about the
cookie policy at EU General Data Protection Regulation (GDPR ) support.
Select the Students link and then Create New.
Test the Edit, Details, and Delete links.
Examine the SchoolContext DB context
The main class that coordinates EF Core functionality for a given data model is the DB context class. The data
context is derived from Microsoft.EntityFrameworkCore.DbContext. The data context specifies which entities are
included in the data model. In this project, the class is named SchoolContext .
Update SchoolContext.cs with the following code:
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options)
: base(options)
{
}
The highlighted code creates a DbSet<TEntity> property for each entity set. In EF Core terminology:
An entity set typically corresponds to a DB table.
An entity corresponds to a row in the table.
DbSet<Enrollment> and DbSet<Course> could be omitted. EF Core includes them implicitly because the Student
entity references the Enrollment entity, and the Enrollment entity references the Course entity. For this tutorial,
keep DbSet<Enrollment> and DbSet<Course> in the SchoolContext .
SQL Server Express LocalDB
The connection string specifies SQL Server LocalDB. LocalDB is a lightweight version of the SQL Server Express
Database Engine and is intended for app development, not production use. LocalDB starts on demand and runs in
user mode, so there's no complex configuration. By default, LocalDB creates .mdf DB files in the C:/Users/<user>
directory.
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// context.Database.EnsureCreated();
The code checks if there are any students in the DB. If there are no students in the DB, the DB is initialized with test
data. It loads test data into arrays rather than List<T> collections to optimize performance.
The EnsureCreated method automatically creates the DB for the DB context. If the DB exists, EnsureCreated returns
without modifying the DB.
In Program.cs, modify the Main method to call Initialize :
try
{
var context = services.GetRequiredService<SchoolContext>();
// using ContosoUniversity.Data;
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
Delete any student records and restart the app. If the DB is not initialized, set a break point in Initialize to
diagnose the problem.
View the DB
Open SQL Server Object Explorer (SSOX) from the View menu in Visual Studio. In SSOX, click
(localdb)\MSSQLLocalDB > Databases > ContosoUniversity1.
Expand the Tables node.
Right-click the Student table and click View Data to see the columns created and the rows inserted into the table.
Asynchronous code
Asynchronous programming is the default mode for ASP.NET Core and EF Core.
A web server has a limited number of threads available, and in high load situations all of the available threads
might be in use. When that happens, the server can't process new requests until the threads are freed up. With
synchronous code, many threads may be tied up while they aren't actually doing any work because they're waiting
for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up
for the server to use for processing other requests. As a result, asynchronous code enables server resources to be
used more efficiently, and the server is enabled to handle more traffic without delays.
Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations, the
performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.
In the following code, the async keyword, Task<T> return value, await keyword, and ToListAsync method make
the code execute asynchronously.
public async Task OnGetAsync()
{
Student = await _context.Student.ToListAsync();
}
Some things to be aware of when writing asynchronous code that uses EF Core:
Only statements that cause queries or commands to be sent to the DB are executed asynchronously. That
includes, ToListAsync , SingleOrDefaultAsync , FirstOrDefaultAsync , and SaveChangesAsync . It doesn't include
statements that just change an IQueryable , such as
var students = context.Students.Where(s => s.LastName == "Davolio") .
An EF Core context isn't thread safe: don't try to do multiple operations in parallel.
To take advantage of the performance benefits of async code, verify that library packages (such as for paging)
use async if they call EF Core methods that send queries to the DB.
For more information about asynchronous programming in .NET, see Async Overview and Asynchronous
programming with async and await.
In the next tutorial, basic CRUD (create, read, update, delete) operations are examined.
NEXT
Razor Pages with EF Core in ASP.NET Core - CRUD -
2 of 8
7/30/2018 • 11 minutes to read • Edit Online
The ASP.NET Core 2.0 version of this tutorial can be found in this PDF file.
The ASP.NET Core 2.1 version of this tutorial has many improvements over the 2.0 version.
By Tom Dykstra, Jon P Smith, and Rick Anderson
The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual
Studio. For information about the tutorial series, see the first tutorial.
In this tutorial, the scaffolded CRUD (create, read, update, delete) code is reviewed and customized.
To minimize complexity and keep these tutorials focused on EF Core, EF Core code is used in the page models.
Some developers use a service layer or repository pattern in to create an abstraction layer between the UI (Razor
Pages) and the data access layer.
In this tutorial, the Create, Edit, Delete, and Details Razor Pages in the Student folder are examined.
The scaffolded code uses the following pattern for Create, Edit, and Delete pages:
Get and display the requested data with the HTTP GET method OnGetAsync .
Save changes to the data with the HTTP POST method OnPostAsync .
The Index and Details pages get and display the requested data with the HTTP GET method OnGetAsync
SingleOrDefaultAsync vs FirstOrDefaultAsync
The generated code uses FirstOrDefaultAsync, which is generally preferred over SingleOrDefaultAsync.
FirstOrDefaultAsync is more efficient than SingleOrDefaultAsync at fetching one entity:
Unless the code needs to verify that there's not more than one entity returned from the query.
SingleOrDefaultAsync fetches more data and does unnecessary work.
SingleOrDefaultAsync throws an exception if there's more than one entity that fits the filter part.
FirstOrDefaultAsync doesn't throw if there's more than one entity that fits the filter part.
FindAsync
In much of the scaffolded code, FindAsync can be used in place of FirstOrDefaultAsync .
FindAsync :
Finds an entity with the primary key (PK). If an entity with the PK is being tracked by the context, it's returned
without a request to the DB.
Is simple and concise.
Is optimized to look up a single entity.
Can have perf benefits in some situations, but they rarely happens for typical web apps.
Implicitly uses FirstAsync instead of SingleAsync.
But if you want to Include other entities, then FindAsync is no longer appropriate. This means that you may
need to abandon FindAsync and move to a query as your app progresses.
Customize the Details page
Browse to Pages/Students page. The Edit, Details, and Delete links are generated by the Anchor Tag Helper in
the Pages/Students/Index.cshtml file.
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
Run the app and select a Details link. The URL is of the form http://localhost:5000/Students/Details?id=2 . The
Student ID is passed using a query string ( ?id=2 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive
for each of these pages from @page to @page "{id:int}" .
A request to the page with the "{id:int}" route template that does not include a integer route value returns an
HTTP 404 (not found) error. For example, http://localhost:5000/Students/Details returns a 404 error. To make
the ID optional, append ? to the route constraint:
@page "{id:int?}"
Run the app, click on a Details link, and verify the URL is passing the ID as route data (
http://localhost:5000/Students/Details/2 ).
Don't globally change @page to @page "{id:int}" , doing so breaks the links to the Home and Create pages.
Add related data
The scaffolded code for the Students Index page doesn't include the Enrollments property. In this section, the
contents of the Enrollments collection is displayed in the Details page.
The OnGetAsync method of Pages/Students/Details.cshtml.cs uses the FirstOrDefaultAsync method to retrieve a
single Student entity. Add the following highlighted code:
if (Student == null)
{
return NotFound();
}
return Page();
}
The Include and ThenInclude methods cause the context to load the Student.Enrollments navigation property,
and within each enrollment the Enrollment.Course navigation property. These methods are examined in detail in
the reading-related data tutorial.
The AsNoTracking method improves performance in scenarios when the entities returned are not updated in the
current context. AsNoTracking is discussed later in this tutorial.
Display related enrollments on the Details page
Open Pages/Students/Details.cshtml. Add the following highlighted code to display a list of enrollments:
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Student</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
If code indentation is wrong after the code is pasted, press CTRL -K-D to correct it.
The preceding code loops through the entities in the Enrollments navigation property. For each enrollment, it
displays the course title and the grade. The course title is retrieved from the Course entity that's stored in the
Course navigation property of the Enrollments entity.
Run the app, select the Students tab, and click the Details link for a student. The list of courses and grades for the
selected student is displayed.
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Student.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return null;
}
TryUpdateModelAsync
Examine the TryUpdateModelAsync code:
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
In the preceding code, TryUpdateModelAsync<Student> tries to update the emptyStudent object using the posted
form values from the PageContext property in the PageModel. TryUpdateModelAsync only updates the properties
listed ( s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate ).
In the preceding sample:
The second argument ( "student", // Prefix ) is the prefix uses to look up values. It's not case sensitive.
The posted form values are converted to the types in the Student model using model binding.
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:
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
Even if the app doesn't have a Secret field on the create/update Razor Page, a hacker could set the Secret value
by overposting. A hacker could use a tool such as Fiddler, or write some JavaScript, to post a Secret form value.
The original code doesn't limit the fields that the model binder uses when it creates a Student instance.
Whatever value the hacker specified for the Secret form field is updated in the DB. The following image shows
the Fiddler tool adding the Secret field (with the value "OverPost") to the posted form values.
The value "OverPost" is successfully added to the Secret property of the inserted row. The app designer never
intended the Secret property to be set with the Create page.
View model
A view model typically contains a subset of the properties included in the model used by the application. The
application model is often called the domain model. The domain model typically contains all the properties
required by the corresponding entity in the DB. The view model contains only the properties needed for the UI
layer (for example, the Create page). In addition to the view model, some apps use a binding model or input
model to pass data between the Razor Pages page model class and the browser. Consider the following Student
view model:
using System;
namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
View models provide an alternative way to prevent overposting. The view model contains only the properties to
view (display) or update.
The following code uses the StudentVM view model to create a new student:
[BindProperty]
public StudentVM StudentVM { get; set; }
The SetValues method sets the values of this object by reading values from another PropertyValues object.
SetValues uses property name matching. The view model type doesn't need to be related to the model type, it
just needs to have properties that match.
Using StudentVM requires CreateVM.cshtml be updated to use StudentVM rather than Student .
In Razor Pages, the PageModel derived class is the view model.
[BindProperty]
public Student Student { get; set; }
if (Student == null)
{
return NotFound();
}
return Page();
}
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
}
The code changes are similar to the Create page with a few exceptions:
OnPostAsync has an optional id parameter.
The current student is fetched from the DB, rather than creating an empty student.
FirstOrDefaultAsync has been replaced with FindAsync. FindAsync is a good choice when selecting an entity
from the primary key. See FindAsync for more information.
Test the Edit and Create pages
Create and edit a few student entities.
Entity States
The DB context keeps track of whether entities in memory are in sync with their corresponding rows in the DB.
The DB context sync information determines what happens when SaveChangesAsync is called. For example,
when a new entity is passed to the AddAsync method, that entity's state is set to Added. When SaveChangesAsync
is called, the DB context issues a SQL INSERT command.
An entity may be in one of the following states:
Added : The entity doesn't yet exist in the DB. The SaveChanges method issues an INSERT statement.
Unchanged : No changes need to be saved with this entity. An entity has this status when it's read from the
DB.
: Some or all of the entity's property values have been modified. The
Modified SaveChanges method issues
an UPDATE statement.
Deleted : The entity has been marked for deletion. The SaveChanges method issues a DELETE statement.
Detached : The entity isn't being tracked by the DB context.
In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity
state to automatically be changed to Modified . Calling SaveChanges generates a SQL UPDATE statement that
updates only the changed properties.
In a web app, the DbContext that reads an entity and displays the data is disposed after a page is rendered. When
a page's OnPostAsync method is called, a new web request is made and with a new instance of the DbContext . Re-
reading the entity in that new context simulates desktop processing.
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = "Delete failed. Try again";
}
return Page();
}
The preceding code contains the optional parameter saveChangesError . saveChangesError indicates whether the
method was called after a failure to delete the student object. The delete operation might fail because of transient
network problems. Transient network errors are more likely in the cloud. saveChangesError is false when the
Delete page OnGetAsync is called from the UI. When OnGetAsync is called by OnPostAsync (because the delete
operation failed), the saveChangesError parameter is true.
The Delete pages OnPostAsync method
Replace the OnPostAsync with the following code:
if (student == null)
{
return NotFound();
}
try
{
_context.Student.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
The preceding code retrieves the selected entity, then calls the Remove method to set the entity's status to
Deleted . When SaveChanges is called, a SQL DELETE command is generated. If Remove fails:
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ErrorMessage</p>
Test Delete.
Common errors
Student/Index or other links don't work:
Verify the Razor Page contains the correct @page directive. For example, The Student/Index Razor Page should
not contain a route template:
@page "{id:int}"
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core - Sort,
Filter, Paging - 3 of 8
9/18/2018 • 14 minutes to read • Edit Online
The ASP.NET Core 2.0 version of this tutorial can be found in this PDF file.
The ASP.NET Core 2.1 version of this tutorial has many improvements over the 2.0 version.
By Tom Dykstra, Rick Anderson, and Jon P Smith
The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual
Studio. For information about the tutorial series, see the first tutorial.
In this tutorial, sorting, filtering, grouping, and paging, functionality is added.
The following illustration shows a completed page. The column headings are clickable links to sort the column.
Clicking a column heading repeatedly switches between ascending and descending sort order.
If you run into problems you can't solve, download the completed app.
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
The preceding code receives a sortOrder parameter from the query string in the URL. The URL (including the
query string) is generated by the Anchor Tag Helper
The sortOrder parameter is either "Name" or "Date." The sortOrder parameter is optionally followed by "_desc"
to specify descending order. The default sort order is ascending.
When the Index page is requested from the Students link, there's no query string. The students are displayed in
ascending order by last name. Ascending order by last name is the default (fall-through case) in the switch
statement. When the user clicks a column heading link, the appropriate sortOrder value is provided in the query
string value.
NameSort and DateSort are used by the Razor Page to configure the column heading hyperlinks with the
appropriate query string values:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
The first line specifies that when sortOrder is null or empty, NameSort is set to "name_desc." If sortOrder is not
null or empty, NameSort is set to an empty string.
The ?: operator is also known as the ternary operator.
These two statements enable the page to set the column heading hyperlinks as follows:
The method uses LINQ to Entities to specify the column to sort by. The code initializes an IQueryable<Student>
before the switch statement, and modifies it in the switch statement:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
When an IQueryable is created or modified, no query is sent to the database. The query isn't executed until the
IQueryable object is converted into a collection. IQueryable are converted to a collection by calling a method
such as ToListAsync . Therefore, the IQueryable code results in a single query that's not executed until the
following statement:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
The preceding code would ensure that results are case-insensitive if the code changes to use IEnumerable . When
Contains is called on an IEnumerable collection, the .NET Core implementation is used. When Contains is called
on an IQueryable object, the database implementation is used. Returning an IEnumerable from a repository can
have a significant performance penality:
1. All the rows are returned from the DB server.
2. The filter is applied to all the returned rows in the application.
There's a performance penalty for calling ToUpper . The ToUpper code adds a function in the WHERE clause of the
TSQL SELECT statement. The added function prevents the optimizer from using an index. Given that SQL is
installed as case-insensitive, it's best to avoid the ToUpper call when it's not needed.
Add a Search Box to the Student Index page
In Pages/Students/Index.cshtml, add the following highlighted code to create a Search button and assorted
chrome.
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
The preceding code uses the <form> tag helper to add the search text box and button. By default, the <form> tag
helper submits form data with a POST. With POST, the parameters are passed in the HTTP message body and
not in the URL. When HTTP GET is used, the form data is passed in the URL as query strings. Passing the data
with query strings enables users to bookmark the URL. The W3C guidelines recommend that GET should be used
when the action doesn't result in an update.
Test the app:
Select the Students tab and enter a search string.
Select Search.
Notice that the URL contains the search string.
http://localhost:5000/Students?SearchString=an
If the page is bookmarked, the bookmark contains the URL to the page and the SearchString query string. The
method="get" in the form tag is what caused the query string to be generated.
Currently, when a column heading sort link is selected, the filter value from the Search box is lost. The lost filter
value is fixed in the next section.
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
this.AddRange(items);
}
The CreateAsyncmethod in the preceding code takes page size and page number and applies the appropriate
Skip and Take statements to the IQueryable . When ToListAsync is called on the IQueryable , it returns a List
containing only the requested page. The properties HasPreviousPage and HasNextPage are used to enable or
disable Previous and Next paging buttons.
The CreateAsync method is used to create the PaginatedList<T> . A constructor can't create the PaginatedList<T>
object, constructors can't run asynchronous code.
CurrentFilter = searchString;
int pageSize = 3;
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
The preceding code adds the page index, the current sortOrder , and the currentFilter to the method signature.
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
The PaginatedList.CreateAsync method converts the student query to a single page of students in a collection
type that supports paging. That single page of students is passed to the Razor Page.
The two question marks in PaginatedList.CreateAsync represent the null-coalescing operator. The null-coalescing
operator defines a default value for a nullable type. The expression (pageIndex ?? 1) means return the value of
pageIndex if it has a value. If pageIndex doesn't have a value, return 1.
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
The column header links use the query string to pass the current search string to the OnGetAsync method so that
the user can sort within filter results:
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;
using ContosoUniversity.Data;
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
Run the app and navigate to the About page. The count of students for each enrollment date is displayed in a
table.
If you run into problems you can't solve, download the completed app for this stage.
Additional resources
Debugging ASP.NET Core 2.x source
In the next tutorial, the app uses migrations to update the data model.
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core -
Migrations - 4 of 8
7/10/2018 • 5 minutes to read • Edit Online
The ASP.NET Core 2.0 version of this tutorial can be found in this PDF file.
The ASP.NET Core 2.1 version of this tutorial has many improvements over the 2.0 version.
By Tom Dykstra, Jon P Smith, and Rick Anderson
The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual
Studio. For information about the tutorial series, see the first tutorial.
In this tutorial, the EF Core migrations feature for managing data model changes is used.
If you run into problems you can't solve, download the completed app.
When a new app is developed, the data model changes frequently. Each time the model changes, the model gets
out of sync with the database. This tutorial started by configuring the Entity Framework to create the database if it
doesn't exist. Each time the data model changes:
The DB is dropped.
EF creates a new one that matches the model.
The app seeds the DB with test data.
This approach to keeping the DB in sync with the data model works well until you deploy the app to production.
When the app is running in production, it's usually storing data that needs to be maintained. The app can't start
with a test DB each time a change is made (such as adding a new column). The EF Core Migrations feature solves
this problem by enabling EF Core to update the DB schema instead of creating a new DB.
Rather than dropping and recreating the DB when the data model changes, migrations updates the schema and
retains existing data.
Drop-Database
migrationBuilder.CreateTable(
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Enrollment");
migrationBuilder.DropTable(
name: "Course");
migrationBuilder.DropTable(
name: "Student");
}
}
Migrations calls the Up method to implement the data model changes for a migration. When you enter a
command to roll back the update, migrations calls the Down method.
The preceding code is for the initial migration. That code was created when the migrations add InitialCreate
command was run. The migration name parameter ("InitialCreate" in the example) is used for the file name. The
migration name can be any valid file name. It's best to choose a word or phrase that summarizes what is being
done in the migration. For example, a migration that added a department table might be called
"AddDepartmentTable."
If the initial migration is created and the DB exists:
The DB creation code is generated.
The DB creation code doesn't need to run because the DB already matches the data model. If the DB creation
code is run, it doesn't make any changes because the DB already matches the data model.
When the app is deployed to a new environment, the DB creation code must be run to create the DB.
Previously the DB was dropped and doesn't exist, so migrations creates the new DB.
The data model snapshot
Migrations create a snapshot of the current database schema in Migrations/SchoolContextModelSnapshot.cs.
When you add a migration, EF determines what changed by comparing the data model to the snapshot file.
To delete a migration, use the following command:
Visual Studio
.NET Core CLI
Remove-Migration
The remove migrations command deletes the migration and ensures the snapshot is correctly reset.
Remove EnsureCreated and test the app
For early development, EnsureCreated was used. In this tutorial, migrations are used. EnsureCreated has the
following limitations:
Bypasses migrations and creates the DB and schema.
Doesn't create a migrations table.
Can not be used with migrations.
Is designed for testing or rapid prototyping where the DB is dropped and re-created frequently.
Remove the following line from DbInitializer :
context.Database.EnsureCreated();
EF Core uses the __MigrationsHistory table to see if any migrations need to run. If the DB is up-to-date, no
migration is run.
Troubleshooting
Download the completed app.
The app generates the following exception:
SqlException: Cannot open database "ContosoUniversity" requested by the login.
The login failed.
Login failed for user 'user name'.
Additional resources
.NET Core CLI.
Package Manager Console (Visual Studio)
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core - Data
Model - 5 of 8
7/24/2018 • 28 minutes to read • Edit Online
The ASP.NET Core 2.0 version of this tutorial can be found in this PDF file.
The ASP.NET Core 2.1 version of this tutorial has many improvements over the 2.0 version.
By Tom Dykstra and Rick Anderson
The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual
Studio. For information about the tutorial series, see the first tutorial.
The previous tutorials worked with a basic data model that was composed of three entities. In this tutorial:
More entities and relationships are added.
The data model is customized by specifying formatting, validation, and database mapping rules.
The entity classes for the completed data model is shown in the following illustration:
If you run into problems you can't solve, download the completed app.
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 specifies a data type that's more specific than the database intrinsic type. In this case only
the date should be displayed, not the date and time. The DataType Enumeration provides for many data types,
such as Date, Time, PhoneNumber, Currency, EmailAddress, etc. The DataType attribute can also enable the app
to automatically provide type-specific features. For example:
The mailto: link is automatically created for DataType.EmailAddress .
The date selector is provided for DataType.Date in most browsers.
The DataType attribute emits HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers consume.
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 DisplayFormatattribute can be used by itself. It's generally a good idea to use the DataType attribute with the
DisplayFormat attribute. The DataType attribute conveys the semantics of the data as opposed to how to render it
on a screen. The DataType attribute provides the following benefits that are not available in DisplayFormat :
The browser can enable HTML5 features. For example, show a calendar control, the locale-appropriate
currency symbol, email links, client-side input validation, etc.
By default, the browser renders data using the correct format based on the locale.
For more information, see the <input> Tag Helper documentation.
Run the app. Navigate to the Students Index page. Times are no longer displayed. Every view that uses the
Student model displays the date without time.
The StringLength attribute
Data validation rules and validation error messages can be specified with attributes. The StringLength attribute
specifies the minimum and maximum length of characters that are allowed in a data field. The StringLength
attribute also provides client-side and server-side validation. The minimum value has no impact on the database
schema.
Update the Student model with the following code:
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
The preceding code limits names to no more than 50 characters. The StringLength attribute doesn't prevent a
user from entering white space for a name. The RegularExpression attribute is used to apply restrictions to the
input. For example, the following code requires the first character to be upper case and the remaining characters
to be alphabetical:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
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) because
migrations has not been run on the DB. When migrations are run later in this tutorial, the name fields become
nvarchar(50) .
The Column attribute
Attributes can control how classes and properties are mapped to the database. In this section, the Column
attribute is used to map the name of the FirstMidName property to "FirstName" in the DB.
When the DB 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.
Update the Student.cs file with the following highlighted code:
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
With the preceding change, Student.FirstMidName in the app 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. If the app is run before applying migrations, the following
exception is generated:
Add-Migration ColumnFirstName
Update-Database
The migrations add ColumnFirstName command generates the following warning message:
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
The warning is generated because the name fields are now limited to 50 characters. If a name in the DB had more
than 50 characters, the 51 to last character would be lost.
Test the app.
Open the Student table in SSOX:
Before migration was applied, the name columns were of type nvarchar(MAX). The name columns are now
nvarchar(50) . The column name has changed from FirstMidName to FirstName .
NOTE
In the following section, building the app at some stages generates compiler errors. The instructions specify when to build
the app.
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;
}
}
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:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
[Key]
public int InstructorID { get; set; }
By default, EF Core treats the key as non-database-generated because the column is for an identifying
relationship.
The Instructor navigation property
The OfficeAssignment navigation property for the Instructor entity is nullable because:
Reference types (such as classes are nullable).
An instructor might not have an office assignment.
The OfficeAssignment entity has a non-nullable Instructor navigation property because:
InstructorIDis non-nullable.
An office assignment can't exist without an instructor.
When an Instructor entity has a related OfficeAssignment entity, each entity has a reference to the other one in
its navigation property.
The [Required] attribute could be applied to the Instructor navigation property:
[Required]
public Instructor Instructor { get; set; }
The preceding code specifies that there must be a related instructor. The preceding code is unnecessary because
the InstructorID foreign key (which is also the PK) is non-nullable.
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 FK 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. Having the FK in the data model can make updates simpler and more efficient. For
example, consider a model where the FK property DepartmentID is not included. When a course entity is fetched
to edit:
The Department entity is null if it's not explicitly loaded.
To update the course entity, the Department entity must first be fetched.
When the FK property DepartmentID is included in the data model, there's no need to fetch the Department entity
before an update.
The DatabaseGenerated attribute
The [DatabaseGenerated(DatabaseGeneratedOption.None)] attribute specifies that the PK is provided by the
application rather than generated by the database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
By default, EF Core assumes that PK values are generated by the DB. DB generated PK values 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 DB can
automatically generate a date field to record the date a row was created or updated. For more information, see
Generated Properties.
Foreign key and navigation properties
The foreign key (FK) properties and navigation properties in the Course entity reflect the following relationships:
A course is assigned to one department, so there's a DepartmentID FK and a Department navigation property.
A course can have any number of students enrolled in it, so the Enrollments navigation property is a collection:
A course may be taught by multiple instructors, so the CourseAssignments navigation property is a collection:
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
[Column(TypeName="money")]
public decimal Budget { get; set; }
Column mapping is generally not required. EF Core generally chooses the appropriate SQL Server data type
based on the CLR type for the property. The CLR decimal type maps to a SQL Server decimal type. Budget is
for currency, and the money data type is more appropriate for currency.
Foreign key and navigation properties
The FK and navigation properties reflect the following relationships:
A department may or may not have an administrator.
An administrator is always an instructor. Therefore the InstructorID property is included as the FK to the
Instructor entity.
The question mark (?) in the preceding code specifies the property is nullable.
A department may have many courses, so there's a Courses navigation property:
public ICollection<Course> Courses { get; set; }
Note: By convention, EF Core enables cascade delete for non-nullable FKs and for many-to-many relationships.
Cascading delete can result in circular cascade delete rules. Circular cascade delete rules causes an exception when
a migration is added.
For example, if the Department.InstructorID property wasn't defined as nullable:
EF Core configures a cascade delete rule to delete the instructor when the department is deleted.
Deleting the instructor when the department is deleted isn't the intended behavior.
If business rules required the InstructorID property be non-nullable, use the following fluent API statement:
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
An enrollment record is for one student, so there's a StudentID FK property and a Student navigation property:
Many-to-Many Relationships
There's a many-to-many relationship between the Student and Course entities. The Enrollment entity functions
as a many-to-many join table with payload in the database. "With payload" means that the Enrollment table
contains additional data besides FKs for the joined tables (in this case, the PK and Grade ).
The following illustration shows what these relationships look like in an entity diagram. (This diagram was
generated using EF Power Tools for EF 6.x. Creating the diagram isn't part of the tutorial.)
Each relationship line has a 1 at one end and an asterisk (*) at the other, indicating a one-to-many relationship.
If the table didn't include grade information, it would only need to contain the two FKs ( CourseID and
Enrollment
StudentID ). A many-to-many join table without payload is sometimes called a pure join table ( PJT ).
The Instructor and Course entities have a many-to-many relationship using a pure join table.
Note: EF 6.x supports implicit join tables for many-to-many relationships, but EF Core doesn't. For more
information, see Many-to-many relationships in EF Core 2.0.
The CourseAssignment entity
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; }
}
}
Instructor-to -Courses
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
The preceding code adds the new entities and configures the CourseAssignment entity's composite PK.
In this tutorial, the fluent API is used only for DB mapping that can't be done with attributes. However, the fluent
API can specify most of the formatting, validation, and mapping rules that can be done with attributes.
Some attributes such as MinimumLength can't be applied with the fluent API. MinimumLength doesn't change the
schema, it only applies a minimum length validation rule.
Some developers prefer to use the fluent API exclusively so that they can keep their entity classes "clean."
Attributes and the fluent API can be mixed. There are some configurations that can only be done with the fluent
API (specifying a composite PK). There are some configurations that can only be done with attributes (
MinimumLength ). The recommended practice for using fluent API or attributes:
Choose one of these two approaches.
Use the chosen approach consistently as much as possible.
Some of the attributes used in the this tutorial are used for:
Validation only (for example, MinimumLength ).
EF Core configuration only (for example, HasKey ).
Validation and EF Core configuration (for example, [StringLength(50)] ).
For more information about attributes vs. fluent API, see Methods of configuration.
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();
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. The preceding code creates the following many-to-many
relationships:
Enrollments
CourseAssignment
Add a migration
Build the project.
Visual Studio
.NET Core CLI
Add-Migration ComplexDataModel
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'.
When migrations are run with existing data, there may be FK constraints that are not satisfied with the exiting
data. For this tutorial, a new DB is created, so there are no FK constraint violations. See Fixing foreign key
constraints with legacy data for instructions on how to fix the FK violations on the current DB.
Drop and update the database
The code in the updated DbInitializer adds seed data for the new entities. To force EF Core to create a new DB,
drop and update the DB:
Visual Studio
.NET Core CLI
In the Package Manager Console (PMC ), run the following command:
Drop-Database
Update-Database
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
type: "int",
nullable: false,
defaultValue: 0);
The preceding code adds a non-nullable DepartmentID FK to the Course table. The DB from the previous tutorial
contains rows in Course , so that table cannot be updated by migrations.
To make the ComplexDataModel migration work with existing data:
Change the code to give the new column ( DepartmentID ) a default value.
Create a fake department named "Temp" to act as the default department.
Fix the foreign key constraints
Update the ComplexDataModel classes Up method:
Open the {timestamp }_ComplexDataModel.cs file.
Comment out the line of code that adds the DepartmentID column to the Course table.
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. The new code goes after the .CreateTable( name: "Department" block:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(type: "int", nullable: true),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(type: "datetime2", 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);
With the preceding changes, existing Course rows will be related to the "Temp" department after the
ComplexDataModel Up method runs.
A production app would:
Include code or scripts to add Department rows and related Course rows to the new Department rows.
Not use the "Temp" department or the default value for Course.DepartmentID .
The next tutorial covers related data.
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core - Read
Related Data - 6 of 8
9/21/2018 • 14 minutes to read • Edit Online
Note: EF Core automatically fixes up navigation properties to any other entities that were previously loaded
into the context instance. Even if the data for a navigation property is not explicitly included, the property
may still be populated if some or all of the related entities were previously loaded.
Explicit loading. When the entity is first read, related data isn't retrieved. Code must be written to retrieve
the related data when it's needed. Explicit loading with separate queries results in multiple queries sent to
the DB. With explicit loading, the code specifies the navigation properties to be loaded. Use the Load
method to do explicit loading. For example:
Lazy loading. EF Core doesn't currently support 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 DB each time a navigation property is accessed for the first
time.
The Select operator loads only the related data needed.
The preceding code adds AsNoTracking . AsNoTracking improves performance because the entities returned are
not tracked. The entities are not tracked because they're not updated in the current context.
Update Pages/Courses/Index.cshtml with the following highlighted markup:
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-page="TestCreate">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Course[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Course)
{
<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>
Run the app and select the Courses tab to see the list with department names.
The Select operator 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:
The CourseViewModel :
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
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; }
}
}
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
if (id != null)
{
InstructorID = id.Value;
}
}
}
}
The OnGetAsync method accepts optional route data for the ID of the selected instructor.
Examine the query in the Pages/Instructors/Index.cshtml.cs file:
@{
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.Instructor.Instructors)
{
string selectedRow = "";
if (item.ID == Model.InstructorID)
{
selectedRow = "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-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>
The preceding markup makes the following changes:
Updates the page directive from @page to @page "{id:int?}" . "{id:int?}" is a route template. The route
template changes integer query strings in the URL to route data. For example, clicking on the Select link
for an instructor with only the @page directive produces a URL like the following:
http://localhost:1234/Instructors?id=2
When the page directive is @page "{id:int?}" , the previous URL is:
http://localhost:1234/Instructors/2
Added a Courses column that displays courses taught by each instructor. See Explicit Line Transition with
@: for more about this razor syntax.
Added code that dynamically adds class="success" to the tr element of the selected instructor. This sets
a background color for the selected row using a Bootstrap class.
Added a new hyperlink labeled Select. This link sends the selected instructor's ID to the Index method
and sets a background color.
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. The row style changes.
Add courses taught by selected instructor
Update the OnGetAsync method in Pages/Instructors/Index.cshtml.cs with the following code:
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}
The Where method returns a collection. In the preceding Where method, only a single Instructor entity is
returned. The Single method converts the collection into a single Instructor entity. The Instructor entity
provides access to the CourseAssignments property. CourseAssignments provides access to the related Course
entities.
The Single method is used on a collection when the collection has only one item. The Single method throws an
exception if the collection is empty or if there's more than one item. An alternative is SingleOrDefault , which
returns a default value (null in this case) if the collection is empty. Using SingleOrDefault on an empty collection:
Results in an exception (from trying to find a Courses property on a null reference).
The exception message would less clearly indicate the cause of the problem.
The following code populates the view model's Enrollments property when a course is selected:
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Add the following markup to the end of the Pages/Instructors/Index.cshtml Razor Page:
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
</table>
}
The preceding markup displays a list of courses related to an instructor when an instructor is selected.
Test the app. Click on a Select link on the instructors page.
Show student data
In this section, the app is updated to show the student data for a selected course.
Update the query in the OnGetAsync method in Pages/Instructors/Index.cshtml.cs with the following code:
Update Pages/Instructors/Index.cshtml. Add the following markup to the end of the file:
@if (Model.Instructor.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Instructor.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
The preceding markup displays a list of the students who are enrolled in the selected course.
Refresh the page and select an instructor. Select a course to see the list of enrolled students and their grades.
Using Single
The Single method can pass in the Where condition instead of calling the Where method separately:
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Single(
i => i.ID == id.Value);
Instructor.Courses = instructor.CourseAssignments.Select(
s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Single(
x => x.CourseID == courseID).Enrollments;
}
}
The preceding Single approach provides no benefits over using Where . Some developers prefer the Single
approach style.
Explicit loading
The current code specifies eager loading for Enrollments and Students :
Suppose users rarely want to see enrollments in a course. In that case, an optimization would be to only load the
enrollment data if it's requested. In this section, the OnGetAsync is updated to use explicit loading of Enrollments
and Students .
Update the OnGetAsync with the following code:
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
//.Include(i => i.CourseAssignments)
// .ThenInclude(i => i.Course)
// .ThenInclude(i => i.Enrollments)
// .ThenInclude(i => i.Student)
// .AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = Instructor.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();
}
Instructor.Enrollments = selectedCourse.Enrollments;
}
}
The preceding code drops the ThenInclude method calls for enrollment and student data. If a course is selected,
the highlighted code retrieves:
The Enrollment entities for the selected course.
The Student entities for each Enrollment .
Notice the preceding code comments out .AsNoTracking() . Navigation properties can only be explicitly loaded for
tracked entities.
Test the app. From a users perspective, the app behaves identically to the previous version.
The next tutorial shows how to update related data.
P R E V IO U S NEXT
Razor Pages with EF Core in ASP.NET Core - Update
Related Data - 7 of 8
9/21/2018 • 16 minutes to read • Edit Online
namespace ContosoUniversity.Pages.Courses
{
public class DepartmentNamePageModel : PageModel
{
public SelectList DepartmentNameSL { get; set; }
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 .
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");
}
ViewData["DepartmentID"] is replaced with the strongly typed DepartmentNameSL . Strongly typed models are
preferred over weakly typed. For more information, see Weakly typed data (ViewData and ViewBag).
Update the Courses Create page
Update Pages/Courses/Create.cshtml with the following markup:
@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-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Test the Create page. The Create page displays the department name rather than the department ID.
Update the Courses Edit page.
Update the edit page model with the following code:
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 (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 select the department specified in the drop-
down list.
Update Pages/Courses/Edit.cshtml with the following markup:
@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-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
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 clicks Save.
Test the updated code. Create, edit, and delete a course.
[BindProperty]
public Course Course { get; set; }
if (Course == null)
{
return NotFound();
}
return Page();
}
if (Course != null)
{
_context.Courses.Remove(Course);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
}
if (Course == null)
{
return NotFound();
}
return Page();
}
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<form method="post">
<input type="hidden" asp-for="Course.CourseID" />
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
[BindProperty]
public Instructor Instructor { get; set; }
if (Instructor == null)
{
return NotFound();
}
return Page();
}
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;
}
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
}
@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">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Course and Instructor has a many-to-many relationship. To add and remove relationships, you add and remove
entities from the CourseAssignments join entity set.
Check boxes enable changes to courses an instructor is assigned to. A check box is displayed for every course in
the database. Courses that the instructor is assigned to are checked. The user can select or clear check boxes to
change course assignments. If the number of courses were much greater:
You'd probably use a different user interface to display the courses.
The method of manipulating a join entity to create or delete relationships wouldn't change.
Add classes to support Create and Edit instructor pages
Create SchoolViewModels/AssignedCourseData.cs with the following code:
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 check boxes for assigned courses by an instructor.
Create the Pages/Instructors/InstructorCoursesPageModel.cshtml.cs base class:
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
{
The InstructorCoursesPageModel is the base class you will use for the Edit and Create page models.
PopulateAssignedCourseData reads all Course entities to populate AssignedCourseDataList . For each course, the
code sets the CourseID , title, and whether or not the instructor is assigned to the course. A HashSet is used to
create efficient lookups.
Instructors Edit page model
Update the instructor Edit page model with the following code:
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 (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(_context, selectedCourses, instructorToUpdate);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(_context, instructorToUpdate);
return Page();
}
}
The preceding code handles office assignment changes.
Update the instructor Razor View:
@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="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
NOTE
When you paste the code in Visual Studio, line breaks are changed in a way that breaks the code. Press Ctrl+Z one time to
undo the automatic formatting. Ctrl+Z fixes 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.
With the block of new code selected, press Tab three times to line up the new code with the existing code. Vote on or review
the status of this bug with this link.
The preceding code creates an HTML table that has three columns. Each column has a check box and a caption
containing the course number and title. The check boxes 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 check box is set to
CourseID . When the page is posted, the model binder passes an array that consists of the CourseID values for
only the check boxes that are selected.
When the check boxes are initially rendered, courses assigned to the instructor have checked attributes.
Run the app and test the updated instructors Edit page. Change some course assignments. The changes are
reflected on the Index page.
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.
Update the instructors Create page
Update the instructor Create page model with the following code:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class CreateModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
[BindProperty]
public Instructor Instructor { get; set; }
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");
}
PopulateAssignedCourseData(_context, newInstructor);
return Page();
}
}
}
@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>
<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="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
namespace ContosoUniversity.Pages.Instructors
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
[BindProperty]
public Instructor Instructor { get; set; }
if (Instructor == null)
{
return NotFound();
}
return Page();
}
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Concurrency conflicts
A concurrency conflict occurs when:
A user navigates to the edit page for an entity.
Another user updates the same entity before the first user's change is written to the DB.
If concurrency detection isn't enabled, when concurrent updates occur:
The last update wins. That is, the last update values are saved to the DB.
The first of the current updates are lost.
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 when the browser displays the Index page.
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.
Optimistic concurrency includes the following options:
You can keep track of which property a user has modified and update only the corresponding columns in
the DB.
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:
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.
You can let John's change overwrite Jane's change.
The next time someone browses the English department, they will see 9/1/2013 and the 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.) If you don't do any coding for concurrency handling,
Client Wins happens automatically.
You can prevent John's change from being updated in the DB. 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.) You implement the Store Wins scenario in this tutorial. This method ensures that no changes
are overwritten without a user being alerted.
Handling concurrency
When a property is configured as a concurrency token:
EF Core verifies that property has not been modified after it was fetched. The check occurs when
SaveChanges or SaveChangesAsync is called.
If the property has been changed after it was fetched, a DbUpdateConcurrencyException is thrown.
The DB and data model must be configured to support throwing DbUpdateConcurrencyException .
Detecting concurrency conflicts on a property
Concurrency conflicts can be detected at the property level with the ConcurrencyCheck attribute. The attribute
can be applied to multiple properties on the model. For more information, see Data Annotations-
ConcurrencyCheck.
The [ConcurrencyCheck] attribute isn't used in this tutorial.
Detecting concurrency conflicts on a row
To detect concurrency conflicts, a rowversion tracking column is added to the model. rowversion :
Is SQL Server specific. Other databases may not provide a similar feature.
Is used to determine that an entity has not been changed since it was fetched from the DB.
The DB generates a sequential rowversion number that's incremented each time the row is updated. In an
Update or Delete command, the Where clause includes the fetched value of rowversion . If the row being
updated has changed:
rowversion doesn't match the fetched value.
The Update or Delete commands don't find a row because the Where clause includes the fetched
rowversion .
A DbUpdateConcurrencyException is thrown.
In EF Core, when no rows have been updated by an Update or Delete command, a concurrency exception is
thrown.
Add a tracking property to the Department entity
In Models/Department.cs, add a tracking property named RowVersion:
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 is included in the Where clause of Update and Delete
commands. The attribute is called Timestamp because previous versions of SQL Server used a SQL timestamp
data type before the SQL rowversion type replaced it.
The fluent API can also specify the tracking property:
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
The following code shows a portion of the T-SQL generated by EF Core when the Department name is updated:
The preceding highlighted code shows the WHERE clause containing RowVersion . If the DB RowVersion doesn't
equal the RowVersion parameter ( @p2 ), no rows are updated.
The following highlighted code shows the T-SQL that verifies exactly one row was updated:
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT returns the number of rows affected by the last statement. In no rows are updated, EF Core
throws a DbUpdateConcurrencyException .
You can see the T-SQL EF Core generates in the output window of Visual Studio.
Update the DB
Adding the RowVersion property changes the DB model, which requires a migration.
Build the project. Enter the following in a command window:
modelBuilder.Entity("ContosoUniversity.Models.Department", b =>
{
b.Property<int>("DepartmentID")
.ValueGeneratedOnAdd();
b.Property<decimal>("Budget")
.HasColumnType("money");
b.Property<int?>("InstructorID");
b.Property<string>("Name")
.HasMaxLength(50);
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();
b.Property<DateTime>("StartDate");
b.HasKey("DepartmentID");
b.HasIndex("InstructorID");
b.ToTable("Department");
});
@{
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>
RowVersion
</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>
@item.RowVersion[7]
</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>
using ContosoUniversity.Data;
using ContosoUniversity.Models;
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 (await TryUpdateModelAsync<Department>(
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.");
}
}
}
To detect a concurrency issue, the OriginalValue is updated with the rowVersion value from the entity it was
fetched. EF Core generates a SQL UPDATE command with a WHERE clause containing the original RowVersion
value. If no rows are affected by the UPDATE command (no rows have the original RowVersion value), a
DbUpdateConcurrencyException exception is thrown.
In the preceding code, Department.RowVersion is the value when the entity was fetched. OriginalValue is the value
in the DB when FirstOrDefaultAsync was called in this method.
The following code gets the client values (the values posted to this method) and the DB values:
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();
}
The follwing code adds a custom error message for each column that has DB values different from what was
posted to OnPostAsync :
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 RowVersion value to the new value retrieved from the DB. The next time
the user clicks Save, only concurrency errors that happen since the last display of the Edit page will be caught.
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();
}
The ModelState.Remove statement is required because ModelState has the old RowVersion value. In the Razor
Page, the ModelState value for a field takes precedence over the model property values when both are present.
The browser shows the Index page with the changed value and updated rowVersion indicator. Note the updated
rowVersion indicator, it's displayed on the second postback in the other tab.
Change a different field in the second browser tab.
Click Save. You see error messages for all fields that don't match the DB values:
This browser window didn't intend to change the Name field. Copy and paste the current value (Languages) into
the Name field. Tab out. Client-side validation removes the error message.
Click Save again. The value you entered in the second browser tab is saved. You see the saved values in the Index
page.
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
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.RowVersion is the row version when the entity was fetched. When EF Core creates the SQL DELETE
command, it includes a WHERE clause with RowVersion . If the SQL DELETE command results in zero rows
affected:
The RowVersion in the SQL DELETE command doesn't match RowVersion in the DB.
A DbUpdateConcurrencyException exception is thrown.
OnGetAsync is called with the concurrencyError .
Update the Delete page
Update Pages/Departments/Delete.cshtml with the following code:
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
P R E V IO U S
ASP.NET Core MVC with EF Core - tutorial series
6/21/2018 • 2 minutes to read • Edit Online
This tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages is a
new alternative in ASP.NET Core 2.0, a page-based programming model that makes building web UI easier and
more productive. We recommend the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
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
ASP.NET Core MVC with Entity Framework Core -
Tutorial 1 of 10
8/31/2018 • 22 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor
Pages is a page-based programming model that makes building web UI easier and more productive. We
recommend the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
This tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages is
a new alternative in ASP.NET Core 2.0, a page-based programming model that makes building web UI easier
and more productive. We recommend the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
The Contoso University sample web application demonstrates how to create ASP.NET Core 2.0 MVC web
applications using Entity Framework (EF ) Core 2.0 and Visual Studio 2017.
The sample application 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 application from scratch.
Download or view the completed application.
EF Core 2.0 is the latest version of EF but doesn't yet have all the features of EF 6.x. For information about how
to choose between EF 6.x and EF Core, see EF Core vs. EF6.x. If you choose EF 6.x, see the previous version of
this tutorial series.
NOTE
For the ASP.NET Core 1.1 version of this tutorial, see the VS 2017 Update 2 version of this tutorial in PDF format.
Prerequisites
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac
Troubleshooting
If you run into a problem you can't resolve, you can generally find the solution by comparing your code to the
completed project. For a list of common errors and how to solve them, see the Troubleshooting section of the
last tutorial in the series. If you don't find what you need there, you can post a question to StackOverflow.com for
ASP.NET Core or EF Core.
TIP
This is a series of 10 tutorials, each of which builds on what is done in earlier tutorials. Consider saving a copy of the
project after each successful tutorial completion. Then if you run into problems, you can start over from the previous
tutorial instead of going back to the beginning of the whole series.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Contoso
University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Students" asp-action="Index">Students</a></li>
<li><a asp-area="" asp-controller="Courses" asp-action="Index">Courses</a></li>
<li><a asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a></li>
<li><a asp-area="" asp-controller="Departments" asp-action="Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - Contoso University</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
In Views/Home/Index.cshtml, replace the contents of the file with the following code to replace the text about
ASP.NET and MVC with text about this application:
@{
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/aspnet/Docs/tree/master/aspnetcore/data/ef-
mvc/intro/samples/cu-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. You see the
home page with tabs for the pages you'll create in these tutorials.
Entity Framework Core NuGet packages
To add EF Core support to a project, install the database provider that you want to target. This tutorial uses SQL
Server, and the provider package is Microsoft.EntityFrameworkCore.SqlServer. This package is included in the
Microsoft.AspNetCore.All metapackage, so you don't have to install it.
This package and its dependencies ( Microsoft.EntityFrameworkCore and
Microsoft.EntityFrameworkCore.Relational ) provide runtime support for EF. You'll add a tooling package later, in
the Migrations tutorial.
For information about other database providers that are available for Entity Framework Core, see Database
providers.
In the Models folder, create a class file named Student.cs and replace the template code with the following code.
using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
The ID property will become the primary key column of the database table that corresponds to this class. By
default, the Entity Framework interprets a property that's named ID or classnameID as the primary key.
The Enrollments property is a navigation property. Navigation properties hold other entities that are related to
this entity. In this case, the Enrollments property of a Student entity will hold all of the Enrollment entities that
are related to that Student entity. In other words, if a given Student row in the database has two related
Enrollment rows (rows that contain that student's primary key value in their StudentID foreign key column), that
Student entity's Enrollments navigation property will contain those two Enrollment entities.
If a navigation property can hold multiple entities (as in many-to-many or one-to-many relationships), its type
must be a list in which entries can be added, deleted, and updated, such as ICollection<T> . 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 Enrollment entity
In the Models folder, create Enrollment.cs and replace the existing code with the following code:
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
The EnrollmentID property will be the primary key; this entity uses the classnameID pattern instead of ID by
itself as you saw in the Student entity. Ordinarily you would choose one pattern and use it throughout your data
model. Here, the variation illustrates that you can use either pattern. In a later tutorial, you'll see how using ID
without classname makes it easier to implement inheritance in the data model.
The Grade property is an enum . The question mark after the Grade type declaration indicates that the Grade
property is nullable. A grade that's null is different from a zero grade -- null means a grade isn't known or hasn't
been assigned yet.
The StudentID property is a foreign key, and the corresponding navigation property is Student . An Enrollment
entity is associated with one Student entity, so the property can only hold a single Student entity (unlike the
Student.Enrollments navigation property you saw earlier, which can hold multiple Enrollment entities).
The CourseID property is a foreign key, and the corresponding navigation property is Course . An Enrollment
entity is associated with one Course entity.
Entity Framework interprets a property as a foreign key property if it's named
<navigation property name><primary key property name> (for example, StudentID for the Student navigation
property since the Student entity's primary key is ID ). Foreign key properties can also be named simply
<primary key property name> (for example, CourseID since the Course entity's primary key is CourseID ).
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
The Enrollments property is a navigation property. A Course entity can be related to any number of
Enrollment entities.
We'll say more about the DatabaseGenerated attribute in a later tutorial in this series. Basically, this attribute lets
you enter the primary key for the course rather than having the database generate it.
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
This code creates a DbSet property for each entity set. In Entity Framework terminology, an entity set typically
corresponds to a database table, and an entity corresponds to a row in the table.
You could've omitted the DbSet<Enrollment> and DbSet<Course> statements and it would work the same. The
Entity Framework would include them implicitly because the Student entity references the Enrollment entity
and the Enrollment entity references the Course entity.
When the database is created, EF creates tables that have names the same as the DbSet property names.
Property names for collections are typically plural (Students rather than Student), but developers disagree about
whether table names should be pluralized or not. For these tutorials you'll override the default behavior by
specifying singular table names in the DbContext. To do that, add the following highlighted code after the last
DbSet property.
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
services.AddMvc();
}
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.
Add using statements for ContosoUniversity.Data and Microsoft.EntityFrameworkCore namespaces, and then
build the project.
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
Open the appsettings.json file and add a connection string as shown in the following example.
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}
using ContosoUniversity.Models;
using System;
using System.Linq;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();
The code checks if there are any students in the database, and if not, it assumes the database is new and needs to
be seeded with test data. It loads test data into arrays rather than List<T> collections to optimize performance.
In Program.cs, modify the Main method to do the following on application startup:
Get a database context instance from the dependency injection container.
Call the seed method, passing to it the context.
Dispose the context when the seed method is done.
host.Run();
}
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Data;
In older tutorials, you may see similar code in the Configure method in Startup.cs. We recommend that you use
the Configure method only to set up the request pipeline. Application startup code belongs in the Main
method.
Now the first time you run the application, the database will be created and seeded with test data. Whenever you
change your data model, you can delete the database, update your seed method, and start afresh with a new
database the same way. In later tutorials, you'll see how to modify the database when the data model changes,
without deleting and re-creating it.
When you click Add, the Visual Studio scaffolding engine creates a StudentsController.cs file and a set of
views (.cshtml files) that work with the controller.
(The scaffolding engine can also create the database context for you if you don't create it manually first as you
did earlier for this tutorial. You can specify a new context class in the Add Controller box by clicking the plus
sign to the right of Data context class. Visual Studio will then create your DbContext class as well as the
controller and views.)
You'll notice that the controller takes a SchoolContext as a constructor parameter.
namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;
ASP.NET Core dependency injection takes care of passing an instance of SchoolContext into the controller. You
configured that in the Startup.cs file earlier.
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:
You'll learn about the asynchronous programming elements in this code later in the tutorial.
The Views/Students/Index.cshtml view displays this list in a table:
@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 Student 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 you started the application, the DbInitializer.Initialize method calls EnsureCreated . EF saw that there
was no database and so it created one, then the remainder of the Initialize method code populated the
database with data. You can use SQL Server Object Explorer (SSOX) to view the database in Visual Studio.
Close the browser.
If the SSOX window isn't already open, select it from the View menu in Visual Studio.
In SSOX, click (localdb)\MSSQLLocalDB > Databases, and then click the entry for the database name that's
in the connection string in your appsettings.json file.
Expand the Tables node to see the tables in your database.
Right-click the Student table and click View Data to see the columns that were created and the rows that were
inserted into the table.
The .mdf and .ldf database files are in the C:\Users\ folder.
Because you're calling EnsureCreated in the initializer method that runs on app start, you could now make a
change to the Student class, delete the database, run the application again, and the database would
automatically be re-created to match your change. For example, if you add an EmailAddress property to the
Student class, you'll see a new EmailAddress column in the re-created table.
Conventions
The amount of code you had to write in order for the Entity Framework to be able to create a complete database
for you is minimal because of the use of conventions, or assumptions that the Entity Framework makes.
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 primary key properties.
A property is interpreted as a foreign key property if it's named (for example, StudentID for the Student
navigation property since the Student entity's primary key is ID ). Foreign key properties can also be
named simply (for example, EnrollmentID since the Enrollment entity's primary key is EnrollmentID ).
Conventional behavior can be overridden. For example, you can explicitly specify table names, as you saw earlier
in this tutorial. And you can set column names and set any property as primary key or foreign key, as you'll see
in a later tutorial in this series.
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, the async keyword, Task<T> return value, await keyword, and ToListAsync method
make the code execute asynchronously.
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 you are writing asynchronous code that uses the Entity Framework:
Only statements that cause queries or commands to be sent to the database are executed asynchronously.
That includes, for example, ToListAsync , SingleOrDefaultAsync , and SaveChangesAsync . It doesn't include,
for example, statements that just change an IQueryable , such as
var students = context.Students.Where(s => s.LastName == "Davolio") .
An EF context isn't thread safe: don't try to do multiple operations in parallel. When you call any async EF
method, always use the await keyword.
If you want to take advantage of the performance benefits of async code, make sure that any library
packages that you're using (such as for paging), also use async if they call any Entity Framework methods
that cause queries to be sent to the database.
For more information about asynchronous programming in .NET, see Async Overview.
Summary
You've now created a simple application that uses the Entity Framework Core and SQL Server Express LocalDB
to store and display data. In the following tutorial, you'll learn how to perform basic CRUD (create, read, update,
delete) operations.
NEXT
ASP.NET Core MVC with EF Core - CRUD - 2 of 10
8/16/2018 • 20 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
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.
NOTE
It's a common practice to implement the repository pattern in order to create an abstraction layer between your controller
and the data access layer. To keep these tutorials simple and focused on teaching how to use the Entity Framework itself,
they don't use repositories. For information about repositories with EF, see the last tutorial in this series.
if (student == null)
{
return NotFound();
}
return View(student);
}
The Include and ThenInclude methods cause the context to load the Student.Enrollments navigation property,
and within each enrollment the Enrollment.Course navigation property. You'll learn more about these methods in
the read related data tutorial.
The AsNoTracking method improves performance in scenarios where the entities returned won't be updated in
the current context's lifetime. You'll learn more about AsNoTracking at the end of this tutorial.
Route data
The key value that's passed to the Details method comes from route data. Route data is data that the model
binder found in a segment of the URL. For example, the default route specifies controller, action, and id segments:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{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 Details 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.
<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.
<a href="/Students/Edit?studentID=6">Edit</a>
For more information about tag helpers, see Tag helpers in ASP.NET Core.
Add enrollments to the Details view
Open Views/Students/Details.cshtml. Each field is displayed using DisplayNameFor and DisplayFor helpers, as
shown in the following example:
<dt>
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
@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:
<dt>
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd>
<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:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}
This code adds the Student entity created by the ASP.NET Core MVC model binder to the Students entity set and
then saves the changes to the database. (Model binder refers to the ASP.NET Core MVC functionality that makes
it easier for you to work with data submitted by a form; a model binder converts posted form values to CLR types
and passes them to the action method in parameters. In this case, the model binder instantiates a Student entity
for you using property values from the Form collection.)
You removed ID from the Bind attribute because ID is the primary key value which SQL Server will set
automatically when the row is inserted. Input from the user doesn't set the ID value.
Other than the Bind attribute, the try-catch block is the only change you've made to the scaffolded code. If an
exception that derives from DbUpdateException is caught while the changes are being saved, a generic error
message is displayed. DbUpdateException exceptions are sometimes caused by something external to the
application rather than a programming error, so the user is advised to try again. Although not implemented in this
sample, a production quality application would log the exception. For more information, see the Log for insight
section in Monitoring and Telemetry (Building Real-World Cloud Apps with Azure).
The ValidateAntiForgeryToken attribute helps prevent cross-site request forgery (CSRF ) attacks. The token is
automatically injected into the view by the FormTagHelper and is included when the form is submitted by the
user. The token is validated by the ValidateAntiForgeryToken attribute. For more information about CSRF, see
Anti-Request Forgery.
Security note about overposting
The Bind attribute that the scaffolded code includes on the Create method is one way to protect against
overposting in create scenarios. For example, suppose the Student entity includes a Secret property that you
don't want this web page to set.
Even if you don't have a Secret field on the web page, a hacker could use a tool such as Fiddler, or write some
JavaScript, to post a Secret form value. Without the Bind attribute limiting the fields that the model binder uses
when it creates a Student instance, the model binder would pick up that Secret form value and use it to create
the Student entity instance. Then whatever value the hacker specified for the Secret form field would be updated
in your database. The following image shows the Fiddler tool adding the Secret field (with the value "OverPost")
to the posted form values.
The value "OverPost" would then be successfully added to the Secret property of the inserted row, although you
never intended that the web page be able to set that property.
You can prevent overposting in edit scenarios by reading the entity from the database first and then calling
TryUpdateModel , passing in an explicit allowed properties list. That's the method used in these tutorials.
An alternative way to prevent overposting that's preferred by many developers is to use view models rather than
entity classes with model binding. Include only the properties you want to update in the view model. Once the
MVC model binder has finished, copy the view model properties to the entity instance, optionally using a tool
such as AutoMapper. Use _context.Entry on the entity instance to set its state to Unchanged , and then set
Property("PropertyName").IsModified to true on each entity property that's included in the view model. This
method works in both edit and create scenarios.
Test the Create page
The code in Views/Students/Create.cshtml uses label , input , and span (for validation messages) tag helpers
for each field.
Run the app, select the Students tab, and click Create New.
Enter names and a date. Try entering an invalid date if your browser lets you do that. (Some browsers force you to
use a date picker.) Then click Create to see the error message.
This is server-side validation that you get by default; in a later tutorial you'll see how to add attributes that will
generate code for client-side validation also. The following highlighted code shows the model validation check in
the Create method.
[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.
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var studentToUpdate = await _context.Students.SingleOrDefaultAsync(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
whitelisted 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 .
You can use this approach when the web page UI includes all of the fields in the entity and can update any of
them.
The scaffolded code uses the create-and-attach approach but only catches DbUpdateConcurrencyException
exceptions and returns 404 error codes. The example shown catches any database update exception and displays
an error message.
Entity States
The database context keeps track of whether entities in memory are in sync with their corresponding rows in the
database, and this information determines what happens when you call the SaveChanges method. For example,
when you pass a new entity to the Add method, that entity's state is set to Added . Then when you call the
SaveChanges method, the database context issues a SQL INSERT command.
Change some of the data and click Save. The Index page opens and you see the changed data.
if (saveChangesError.GetValueOrDefault())
{
ViewData["ErrorMessage"] =
"Delete failed. Try again, and if the problem persists " +
"see your system administrator.";
}
return View(student);
}
This code accepts an optional parameter that indicates whether the method was called after a failure to save
changes. This parameter is false when the HttpGet Delete method is called without a previous failure. When it's
called by the HttpPost Delete method in response to a database update error, the parameter is true and an error
message is passed to the view.
The read-first approach to HttpPost Delete
Replace the HttpPost Delete action method (named DeleteConfirmed ) with the following code, which performs
the actual delete operation and catches any database update errors.
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var student = await _context.Students
.AsNoTracking()
.SingleOrDefaultAsync(m => m.ID == 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.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{
Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
}
}
If the entity has related data that should also be deleted, make sure that cascade delete is configured in the
database. With this approach to entity deletion, EF might not realize there are related entities to be deleted.
Update the Delete view
In Views/Student/Delete.cshtml, add an error message between the h2 heading and the h3 heading, as shown in
the following example:
<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.)
Handling 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.
Summary
You now have a complete set of pages that perform simple CRUD operations for Student entities. In the next
tutorial you'll expand the functionality of the Index page by adding sorting, filtering, and paging.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Sort, Filter,
Paging - 3 of 10
6/28/2018 • 15 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
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.
Add Column Sort Links to the Students Index Page
To add sorting to the Student Index page, you'll change the Index method of the Students controller and add
code to the Student Index view.
Add sorting Functionality to the Index method
In StudentsController.cs, replace the Index method with the following code:
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.
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.
Add column heading hyperlinks to the Student Index view
Replace the code in Views/Students/Index.cshtml, with the following code to add column heading hyperlinks. The
changed lines are highlighted.
@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 the Students Index page
To add filtering to the Students Index page, you'll add a text box and a submit button to the view and make
corresponding changes in the Index method. The text box will let you enter a string to search for in the first name
and last name fields.
Add filtering functionality to the Index method
In StudentsController.cs, replace the Index method with the following code (the changes are highlighted).
NOTE
Here you are calling the Where method on an IQueryable object, and the filter will be processed on the server. In some
scenarios you might be calling the Where method as an extension method on an in-memory collection. (For example,
suppose you change the reference to _context.Students so that instead of an EF DbSet it references a repository
method that returns an IEnumerable collection.) The result would normally be the same but in some cases may be
different.
For example, the .NET Framework implementation of the Contains method performs a case-sensitive comparison by
default, but in SQL Server this is determined by the collation setting of the SQL Server instance. That setting defaults to
case-insensitive. You could call the ToUpper method to make the test explicitly case-insensitive: Where(s =>
s.LastName.ToUpper().Contains (searchString.ToUpper()). That would ensure that results stay the same if you change the
code later to use a repository which returns an IEnumerable collection instead of an IQueryable object. (When you call
the Contains method on an IEnumerable collection, you get the .NET Framework implementation; when you call it on
an IQueryable object, you get the database provider implementation.) However, there's a performance penalty for this
solution. The ToUpper code would put a function in the WHERE clause of the TSQL SELECT statement. That would prevent
the optimizer from using an index. Given that SQL is mostly installed as case-insensitive, it's best to avoid the ToUpper
code until you migrate to a case-sensitive data store.
<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.
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.
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
this.AddRange(items);
}
The CreateAsync method in this code takes page size and page number and applies the appropriate Skip and
Take statements to the IQueryable . When ToListAsync is called on the IQueryable , it will return a List
containing only the requested page. The properties HasPreviousPage and HasNextPage can be used to enable or
disable Previous and Next paging buttons.
A CreateAsync method is used instead of a constructor to create the PaginatedList<T> object because
constructors can't run asynchronous code.
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
}
This code adds a page number parameter, a current sort order parameter, and a current filter parameter to the
method signature.
The first time the page is displayed, or if the user hasn't clicked a paging or sorting link, all the parameters will be
null. If a paging link is clicked, the page variable will contain the page number to display.
The ViewData element named CurrentSort provides the view with the current sort order, because this must be
included in the paging links in order to keep the sort order the same while paging.
The ViewData element named CurrentFilter provides the view with the current filter string. This value must be
included in the paging links in order to maintain the filter settings during paging, and it must be restored to the
text box when the page is redisplayed.
If the search string is changed during paging, the page has to be reset to 1, because the new filter can result in
different data to display. The search string is changed when a value is entered in the text box and the Submit
button is pressed. In that case, the searchString parameter isn't null.
if (searchString != null)
{
page = 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.
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
(page ?? 1) means return the value of page if it has a value, or return 1 if page is null.
@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
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-page="@(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-page="@(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:
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.
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
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:
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.
NOTE
In the 1.0 version of Entity Framework Core, the entire result set is returned to the client, and grouping is done on the
client. In some scenarios this could create performance problems. Be sure to test performance with production volumes of
data, and if necessary use raw SQL to do the grouping on the server. For information about how to use raw SQL, see the
last tutorial in this series.
Modify the About View
Replace the code in the Views/Home/About.cshtml file with the following code:
@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
@{
ViewData["Title"] = "Student Body Statistics";
}
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
Run the app and go to the About page. The count of students for each enrollment date is displayed in a table.
Summary
In this tutorial, you've seen how to perform sorting, filtering, paging, and grouping. In the next tutorial, you'll learn
how to handle data model changes by using migrations.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Migrations - 4 of
10
9/18/2018 • 8 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
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.
Introduction to 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.
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
(The version numbers in this example were current when the tutorial was written.)
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity2;Trusted_Connection=True;MultipleActiveResultSets=true"
},
This change sets up the project so that the first migration will create a new database. This isn't required to get
started with migrations, but you'll see later why it's a good idea.
NOTE
As an alternative to changing the database name, you can delete the database. Use SQL Server Object Explorer (SSOX)
or the database drop CLI command:
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key
repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'
NOTE
If you see an error message No executable found matching command "dotnet-ef", see this blog post for help
troubleshooting.
If you see an error message "cannot access the file ... ContosoUniversity.dll because it is being used by another
process.", find the IIS Express icon in the Windows System Tray, and right-click it, then click ContosoUniversity
> Stop Site.
Examine the Up and Down methods
When you executed the migrations add command, EF generated the code that will create the database from
scratch. This code is in the Migrations folder, in the file named <timestamp>_InitialCreate.cs. The Up method of
the InitialCreate class creates the database tables that correspond to the data model entity sets, and the Down
method deletes them, as shown in the following example.
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 changed the name of the database in the connection string earlier -- so
that migrations can create a new one from scratch.
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 Introduction to logging.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\username\AppData\Local\ASP.NET\DataProtection-Keys' as key
repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.EntityFrameworkCore.Infrastructure[100403]
Entity Framework Core 2.0.0-rtm-26452 initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (467ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
Executed DbCommand (20ms) [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[200101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20170816151242_InitialCreate', N'2.0.0-rtm-26452');
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.
Command-line interface (CLI) vs. Package Manager Console (PMC)
The EF tooling for managing migrations is available from .NET Core CLI commands or from PowerShell cmdlets
in the Visual Studio Package Manager Console (PMC ) window. This tutorial shows how to use the CLI, but
you can use the PMC if you prefer.
The EF commands for the PMC commands are in the Microsoft.EntityFrameworkCore.Tools package. This
package is already included in the Microsoft.AspNetCore.All metapackage, so you don't have to install it.
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).
Summary
In this tutorial, you've seen how to create and apply your first migration. In the next tutorial, you'll begin looking
at more advanced topics by expanding the data model. Along the way you'll create and apply additional
migrations.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Data Model - 5
of 10
9/18/2018 • 31 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
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:
Customize the Data Model by Using Attributes
In this section you'll see how to customize the data model by using attributes that specify formatting, validation,
and database mapping rules. Then in several of the following sections you'll create the complete School data
model by adding attributes to the classes you already created and creating new classes for the remaining entity
types in the model.
The DataType attribute
For student enrollment dates, all of the web pages currently display the time along with the date, although all you
care about for this field is the date. By using data annotation attributes, you can make one code change that will
fix the display format in every view that shows the data. To see an example of how to do that, you'll add an
attribute to the EnrollmentDate property in the Student class.
In Models/Student.cs, add a using statement for the System.ComponentModel.DataAnnotations namespace and add
DataType and DisplayFormat attributes to the EnrollmentDate property, as shown in the following example:
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.
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should also be applied when the value is
displayed in a text box for editing. (You might not want that for some fields -- for example, for currency values,
you might not want the currency symbol in the text box for editing.)
You can use the DisplayFormat attribute by itself, but it's generally a good idea to use the DataType attribute also.
The DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and
provides the following benefits that you don't get with DisplayFormat :
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, some client-side input validation, etc.).
By default, the browser will render data using the correct format based on your locale.
For more information, see the <input> tag helper documentation.
Run the app, go to the Students Index page and notice that times are no longer displayed for the enrollment
dates. The same will be true for any view that uses the Student model.
The StringLength attribute
You can also specify data validation rules and validation error messages using attributes. The StringLength
attribute sets the maximum length in the database and provides client side and server side validation for
ASP.NET Core MVC. You can also specify the minimum string length in this attribute, but the minimum value has
no impact on the database schema.
Suppose you want to ensure that users don't enter more than 50 characters for a name. To add this limitation,
add StringLength attributes to the LastName and FirstMidName properties, as shown in the following example:
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
The StringLengthattribute 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:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
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:
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 enter either name longer than 50 characters. When
you click Create, client side validation shows an error message.
The Column attribute
You can also use attributes to control how your classes and properties are mapped to the database. Suppose you
had used the name FirstMidName for the first-name field because the field might also contain a middle name. But
you want the database column to be named FirstName , because users who will be writing ad-hoc queries against
the database are accustomed to that name. To make this mapping, you can use the Column attribute.
The Column attribute specifies that when the database is created, the column of the Student table that maps to
the FirstMidName property will be named FirstName . In other words, when your code refers to
Student.FirstMidName , the data will come from or be updated in the FirstName column of the Student table. If
you don't specify column names, they're given the same name as the property name.
In the Student.cs file, add a using statement for System.ComponentModel.DataAnnotations.Schema and add the
column name attribute to the FirstMidName property, as shown in the following highlighted code:
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, ErrorMessage = "First name cannot be longer than 50 characters.")]
[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:
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.
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.
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;
}
}
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:
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).
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
[Key]
public int InstructorID { get; set; }
You can also use the Key attribute if the entity does have its own primary key but you want to name the property
something other than classnameID or ID.
By default, EF treats the key as non-database-generated because the column is for an identifying relationship.
The Instructor navigation property
The Instructor entity has a nullable OfficeAssignment navigation property (because an instructor might not have
an office assignment), and the OfficeAssignment entity has a non-nullable Instructor navigation property
(because an office assignment can't exist without an instructor -- InstructorID is non-nullable). When an
Instructor entity has a related OfficeAssignment entity, each entity will have a reference to the other one in its
navigation property.
You could put a [Required] attribute on the Instructor navigation property to specify that there must be a related
instructor, but you don't have to do that because the InstructorID foreign key (which is also the key to this table)
is non-nullable.
In Models/Course.cs, replace the code you added earlier with the following code. The changes are highlighted.
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.
The DatabaseGenerated attribute
The DatabaseGenerated attribute with the None parameter on the CourseID property specifies that primary key
values are provided by the user rather than generated by the database.
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
By default, Entity Framework assumes that primary key values are generated by the database. That's what you
want in most scenarios. However, for Course entities, you'll use a user-specified course number such as a 1000
series for one department, a 2000 series for another department, and so on.
The DatabaseGenerated attribute can also be used to generate default values, as in the case of database columns
used to record the date a row was created or updated. For more information, see Generated Properties.
Foreign key and navigation properties
The foreign key properties and navigation properties in the Course entity reflect the following relationships:
A course is assigned to one department, so there's a DepartmentID foreign key and a Department navigation
property for the reasons mentioned above.
A course can have any number of students enrolled in it, so the Enrollments navigation property is a collection:
A course may be taught by multiple instructors, so the CourseAssignments navigation property is a collection (the
type CourseAssignment is explained later):
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; }
[Column(TypeName="money")]
public decimal Budget { get; set; }
Column mapping is generally not required, because the Entity Framework chooses the appropriate SQL Server
data type based on the CLR type that you define for the property. The CLR decimal type maps to a SQL Server
decimal type. But in this case you know that the column will be holding currency amounts, and the money data
type is more appropriate for that.
Foreign key and navigation properties
The foreign key and navigation properties reflect the following relationships:
A department may or may not have an administrator, and an administrator is always an instructor. Therefore the
InstructorID property is included as the foreign key to the Instructor entity, and a question mark is added after
the int type designation to mark the property as nullable. The navigation property is named Administrator but
holds an Instructor entity:
NOTE
By convention, the Entity Framework enables cascade delete for non-nullable foreign keys and for many-to-many
relationships. This can result in circular cascade delete rules, which will cause an exception when you try to add a migration.
For example, if you didn't define the Department.InstructorID property as nullable, EF would configure a cascade delete rule
to delete the instructor when you delete the department, which isn't what you want to have happen. If your business rules
required the InstructorID property to be non-nullable, you would have to use the following fluent API statement to
disable cascade delete on the relationship:
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:
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 student, so there's a StudentID foreign key property and a Student
navigation property:
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 6.x supports implicit join tables for many-to-many relationships, but EF Core doesn't. For more information,
see the discussion in the EF Core GitHub repository.)
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; }
}
}
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.
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.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
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):
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
If you tried to run the database update command at this point (don't do it yet), you would get the following error:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint
"FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database "ContosoUniversity",
table "dbo.Department", column 'DepartmentID'.
Sometimes when you execute migrations with existing data, you need to insert stub data into the database to
satisfy foreign key constraints. The generated code in the Up method adds a non-nullable DepartmentID foreign
key to the Course table. If there are already rows in the Course table when the code runs, the AddColumn
operation fails because SQL Server doesn't know what value to put in the column that can't be null. For this
tutorial you'll run the migration on a new database, but in a production application you'd have to make the
migration handle existing data, so the following directions show an example of how to do that.
To make this migration work with existing data you have to change the code to give the new column a default
value, and create a stub department named "Temp" to act as the default department. As a result, existing Course
rows will all be related to the "Temp" department after the Up method runs.
Open the {timestamp }_ComplexDataModel.cs file.
Comment out the line of code that adds the DepartmentID column to the Course table.
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:
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.
Save your changes and build the project.
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
},
After you have changed the database name or deleted the database, run the database update command in the
command window to execute the migrations.
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.
Summary
You now have a more complex data model and corresponding database. In the following tutorial, you'll learn
more about how to access related data.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Read Related
Data - 6 of 10
7/14/2018 • 15 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
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.
Eager, explicit, and lazy Loading of related data
There are several ways that Object-Relational Mapping (ORM ) software such as Entity Framework can load
related data into the navigation properties of an entity:
Eager loading. When the entity is read, related data is retrieved along with it. This typically results in a
single join query that retrieves all of the data that's needed. You specify eager loading in Entity Framework
Core by using the Include and ThenInclude methods.
You can retrieve some of the data in separate queries, and EF "fixes up" the navigation properties. That is,
EF automatically adds the separately retrieved entities where they belong in navigation properties of
previously retrieved entities. For the query that retrieves related data, you can use the Load method
instead of a method that returns a list or object, such as ToList or Single .
Explicit loading. When the entity is first read, related data isn't retrieved. You write code that retrieves the
related data if it's needed. As in the case of eager loading with separate queries, explicit loading results in
multiple queries sent to the database. The difference is that with explicit loading, the code specifies the
navigation properties to be loaded. In Entity Framework Core 1.1 you can use the Load method to do
explicit loading. For example:
Lazy loading. When the entity is first read, related data isn't retrieved. However, the first time you attempt
to access a navigation property, the data required for that navigation property is automatically retrieved. A
query is sent to the database each time you try to get data from a navigation property for the first time.
Entity Framework Core 1.0 doesn't support lazy loading.
Performance considerations
If you know you need related data for every entity retrieved, eager loading often offers the best performance,
because a single query sent to the database is typically more efficient than separate queries for each entity
retrieved. For example, suppose that each department has ten related courses. Eager loading of all related data
would result in just a single (join) query and a single round trip to the database. A separate query for courses for
each department would result in eleven round trips to the database. The extra round trips to the database are
especially detrimental to performance when latency is high.
On the other hand, in some scenarios separate queries is more efficient. Eager loading of all related data in one
query might cause a very complex join to be generated, which SQL Server can't process efficiently. Or if you need
to access an entity's navigation properties only for a subset of a set of the entities you're processing, separate
queries might perform better because eager loading of everything up front would retrieve more data than you
need. If performance is critical, it's best to test performance both ways in order to make the best choice.
Open Views/Courses/Index.cshtml and replace the template code with the following code. The changes are
highlighted:
@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>
Run the app and select the Courses tab to see the list with department names.
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; }
}
}
Open InstructorsController.cs and add a using statement for the ViewModels namespace:
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.
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
The method accepts optional route data ( id ) and a query string parameter ( courseID ) that provide the ID
values of the selected instructor and selected course. The parameters are provided by the Select hyperlinks on
the page.
The code begins by creating an instance of the view model and putting in it the list of instructors. The code
specifies eager loading for the Instructor.OfficeAssignment and the Instructor.CourseAssignments navigation
properties. Within the CourseAssignments property, the Course property is loaded, and within that, the
Enrollments and Department properties are loaded, and within each Enrollment entity the Student property is
loaded.
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 .
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 .
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.
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:
.Single(i => i.ID == id.Value)
Instead of:
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.
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
@{
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 = "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>
Added a Courses column that displays courses taught by each instructor. See Explicit Line Transition with
@: for more about this razor syntax.
Added code that dynamically adds class="success" to the tr element of the selected instructor. This sets
a background color for the selected row using a Bootstrap class.
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.
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.
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
</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.
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.
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.
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
The new code drops the ThenInclude method calls for enrollment data from the code that retrieves instructor
entities. 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.
Summary
You've now used eager loading with one query and with multiple queries to read related data into navigation
properties. In the next tutorial you'll learn how to update related data.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Update Related
Data - 7 of 10
6/28/2018 • 19 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the first
tutorial in the series.
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.
Customize the Create and Edit Pages for Courses
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:
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.
The PopulateDepartmentsDropDownList method gets a list of all departments sorted by name, creates a SelectList
collection for a drop-down list, and passes the collection to the view in ViewBag . The method accepts the optional
selectedDepartment parameter that allows the calling code to specify the item that will be selected when the drop-
down list is rendered. The view will pass the name "DepartmentID" to the <select> tag helper, and the helper
then knows to look in the ViewBag object for a SelectList named "DepartmentID".
The HttpGet Create method calls the PopulateDepartmentsDropDownList method without setting the selected item,
because for a new course the department isn't established yet:
The HttpGet Edit method sets the selected item, based on the ID of the department that's already assigned to
the course being edited:
The HttpPost methods for both Create and Edit also include code that sets the selected item when they
redisplay the page after an error. This ensures that when the page is redisplayed to show the error message,
whatever department was selected stays selected.
Add .AsNoTracking to Details and Delete methods
To optimize performance of the Course Details and Delete pages, add AsNoTracking calls in the Details and
HttpGet Delete methods.
return View(course);
}
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(course);
}
<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" />
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.
<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>
There's already a hidden field ( <input type="hidden"> ) for the course number in the Edit view. Adding a <label>
tag helper doesn't eliminate the need for the hidden field because it doesn't cause the course number to be
included in the posted data when the user clicks Save on the Edit page.
In Views/Courses/Delete.cshtml, add a course number field at the top and change department ID to department
name.
@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.
Click Edit on a course in the Courses Index page.
Change data on the page and click Save. The Courses Index page is displayed with the updated course data.
Replace the HttpPost Edit method with the following code to handle office assignment updates:
[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);
}
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
If the office location is blank, sets the Instructor.OfficeAssignment property to null so that the related row
in the OfficeAssignment table will be deleted.
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
<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 Course assignments to the Instructor 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 check boxes, 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 check boxes. A check
box 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 check boxes 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 check boxes, you'll use a view model class.
Create AssignedCourseData.cs in the SchoolViewModels folder and replace the existing code with the following
code:
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.
The code adds eager loading for the Courses navigation property and calls the new PopulateAssignedCourseData
method to provide information for the check box array using the AssignedCourseData view model class.
The code in the PopulateAssignedCourseData method reads through all Course entities in order to load a list of
courses using the view model class. For each course, the code checks whether the course exists in the instructor's
Courses navigation property. To create efficient lookup when checking whether a course is assigned to the
instructor, the courses assigned to the instructor are put into a HashSet collection. The Assigned property is set to
true for courses the instructor is assigned to. The view will use this property to determine which check boxes 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.
[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);
}
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.SingleOrDefault(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 whitelisting overload and CourseAssignments isn't in the include list.
If no check boxes were selected, the code in UpdateInstructorCourses initializes the CourseAssignments navigation
property with an empty collection and returns:
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.SingleOrDefault(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 check box 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.
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.SingleOrDefault(i =>
i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
If the check box for a course wasn't selected, but the course is in the Instructor.CourseAssignments navigation
property, the course is removed from the navigation property.
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.SingleOrDefault(i =>
i.CourseID == course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
NOTE
When you paste the code in Visual Studio, line breaks will be changed in a way that breaks the code. 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. You can check the status of this problem here.
<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 check box followed by a caption that
consists of the course number and title. The check boxes 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 check box 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 check boxes which are selected.
When the check boxes 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.
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.
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor
instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.CourseAssignments = new List<CourseAssignment>();
foreach (var course in selectedCourses)
{
var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID =
int.Parse(course) };
instructor.CourseAssignments.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
_context.Add(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}
This code is similar to what you saw for the Edit methods except that initially no courses are selected. The
HttpGet Create method calls the PopulateAssignedCourseData method not because there might be courses
selected but in order to provide an empty collection for the foreach loop in the view (otherwise the view code
would throw a null reference exception).
The HttpPost Create method adds each selected course to the CourseAssignments navigation property before it
checks for validation errors and adds the new instructor to the database. Courses are added even if there are
model errors so that when there are model errors (for an example, the user keyed an invalid date), and the page is
redisplayed with an error message, any course selections that were made are automatically restored.
Notice that in order to be able to add courses to the CourseAssignments navigation property you have to initialize
the property as an empty collection:
As an alternative to doing this in controller code, you could do it in the Instructor model by changing the property
getter to automatically create the collection if it doesn't exist, as shown in the following example:
private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
get
{
return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
}
set
{
_courseAssignments = value;
}
}
If you modify the CourseAssignments property in this way, you can remove the explicit property initialization code
in the controller.
In Views/Instructor/Create.cshtml, add an office location text box and check boxes for courses before the Submit
button. As in the case of the Edit page, fix the formatting if Visual Studio reformats the code when you paste it.
<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.
Summary
You have now completed the introduction to working with related data. In the next tutorial you'll see how to
handle concurrency conflicts.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Concurrency - 8
of 10
6/28/2018 • 19 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the first
tutorial in the series.
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.
Concurrency conflicts
A concurrency conflict occurs when one user displays an entity's data in order to edit it, and then another user
updates the same entity's data before the first user's change is written to the database. If you don't enable the
detection of such conflicts, whoever updates the database last overwrites the other user's changes. In many
applications, this risk is acceptable: if there are few users, or few updates, or if isn't really critical if some changes
are overwritten, the cost of programming for concurrency might outweigh the benefit. In that case, you don't have
to configure the application to handle concurrency conflicts.
Pessimistic concurrency (locking)
If your application does need to prevent accidental data loss in concurrency scenarios, one way to do that is to use
database locks. This is called pessimistic concurrency. For example, before you read a row from a database, you
request a lock for read-only or for update access. If you lock a row for update access, no other users are allowed to
lock the row either for read-only or update access, because they would get a copy of data that's in the process of
being changed. If you lock a row for read-only access, others can also lock it for read-only access but not for
update.
Managing locks has disadvantages. It can be complex to program. It requires significant database management
resources, and it can cause performance problems as the number of users of an application increases. For these
reasons, not all database management systems support pessimistic concurrency. Entity Framework Core provides
no built-in support for it, and this tutorial doesn't show you how to implement it.
Optimistic Concurrency
The alternative to pessimistic concurrency is optimistic concurrency. Optimistic concurrency means allowing
concurrency conflicts to happen, and then reacting appropriately if they do. For example, Jane visits the
Department Edit page and changes the Budget amount for the English department from $350,000.00 to $0.00.
Before Jane clicks Save, John visits the same page and changes the Start Date field from 9/1/2007 to 9/1/2013.
Jane clicks Save first and sees her change when the browser returns to the Index page.
Then John clicks Save on an Edit page that still shows a budget of $350,000.00. What happens next is determined
by how you handle concurrency conflicts.
Some of the options include the following:
You can keep track of which property a user has modified and update only the corresponding columns in
the database.
In the example scenario, no data would be lost, because different properties were updated by the two users.
The next time someone browses the English department, they will see both Jane's and John's changes -- a
start date of 9/1/2013 and a budget of zero dollars. This method of updating can reduce the number of
conflicts that could result in data loss, but it can't avoid data loss if competing changes are made to the
same property of an entity. Whether the Entity Framework works this way depends on how you implement
your update code. It's often not practical in a web application, because it can require that you maintain
large amounts of state in order to keep track of all original property values for an entity as well as new
values. Maintaining large amounts of state can affect application performance because it either requires
server resources or must be included in the web page itself (for example, in hidden fields) or in a cookie.
You can let John's change overwrite Jane's change.
The next time someone browses the English department, they will see 9/1/2013 and the restored
$350,000.00 value. This is called a Client Wins or Last in Wins scenario. (All values from the client take
precedence over what's in the data store.) As noted in the introduction to this section, if you don't do any
coding for concurrency handling, this will happen automatically.
You can prevent John's change from being updated in the database.
Typically, you would display an error message, show him the current state of the data, and allow him to
reapply his changes if he still wants to make them. This is called a Store Wins scenario. (The data-store
values take precedence over the values submitted by the client.) You'll implement the Store Wins scenario
in this tutorial. This method ensures that no changes are overwritten without a user being alerted to what's
happening.
Detecting concurrency conflicts
You can resolve conflicts by handling DbConcurrencyException exceptions that the Entity Framework throws. In
order to know when to throw these exceptions, the Entity Framework must be able to detect conflicts. Therefore,
you must configure the database and the data model appropriately. Some options for enabling conflict detection
include the following:
In the database table, include a tracking column that can be used to determine when a row has been
changed. You can then configure the Entity Framework to include that column in the Where clause of SQL
Update or Delete commands.
The data type of the tracking column is typically rowversion . The rowversion value is a sequential number
that's incremented each time the row is updated. In an Update or Delete command, the Where clause
includes the original value of the tracking column (the original row version) . If the row being updated has
been changed by another user, the value in the rowversion column is different than the original value, so
the Update or Delete statement can't find the row to update because of the Where clause. When the Entity
Framework finds that no rows have been updated by the Update or Delete command (that is, when the
number of affected rows is zero), it interprets that as a concurrency conflict.
Configure the Entity Framework to include the original values of every column in the table in the Where
clause of Update and Delete commands.
As in the first option, if anything in the row has changed since the row was first read, the Where clause
won't return a row to update, which the Entity Framework interprets as a concurrency conflict. For database
tables that have many columns, this approach can result in very large Where clauses, and can require that
you maintain large amounts of state. As noted earlier, maintaining large amounts of state can affect
application performance. Therefore this approach is generally not recommended, and it isn't the method
used in this tutorial.
If you do want to implement this approach to concurrency, you have to mark all non-primary-key
properties in the entity you want to track concurrency for by adding the ConcurrencyCheck attribute to
them. That change enables the Entity Framework to include all columns in the SQL Where clause of
Update and Delete statements.
In the remainder of this tutorial you'll add a rowversion tracking property to the Department entity, create a
controller and views, and test to verify that everything works correctly.
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:
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:
@{
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.
Replace the existing code for the HttpPost Edit method with the following code:
[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.SingleOrDefaultAsync(i => i.ID
== databaseValues.InstructorID);
ModelState.AddModelError("InstructorID", $"Current value:
{databaseInstructor?.FullName}");
}
The code begins by trying to read the department to be updated. If the SingleOrDefaultAsync method returns null,
the department was deleted by another user. In that case the code uses the posted form values to create a
department entity so that the Edit page can be redisplayed with an error message. As an alternative, you wouldn't
have to re-create the department entity if you display only an error message without redisplaying the department
fields.
The view stores the original RowVersion value in a hidden field, and this method receives that value in the
rowVersion parameter. Before you call SaveChanges , you have to put that original RowVersion property value in
the OriginalValues collection for the entity.
_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.
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.
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).
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
Finally, the code sets the RowVersion value of the departmentToUpdate to the new value retrieved from the
database. This new RowVersion value will be stored in the hidden field when the Edit page is redisplayed, and the
next time the user clicks Save, only concurrency errors that happen since the redisplay of the Edit page will be
caught.
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
The ModelState.Remove statement is required because ModelState has the old RowVersion value. In the view, the
ModelState value for a field takes precedence over the model property values when both are present.
@{
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");}
}
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:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
try
{
if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
{
_context.Departments.Remove(department);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID
});
}
}
In the scaffolded code that you just replaced, this method accepted only a record ID:
public async Task<IActionResult> DeleteConfirmed(int id)
You've changed this parameter to a Department entity instance created by the model binder. This gives EF access
to the RowVersion property value in addition to the record key.
You have also changed the action method name from DeleteConfirmed to Delete . The scaffolded code used the
name DeleteConfirmed to give the HttpPost method a unique signature. (The CLR requires overloaded methods
to have different method parameters.) Now that the signatures are unique, you can stick with the MVC convention
and use the same name for the HttpPost and HttpGet delete methods.
If the department is already deleted, the AnyAsync method returns false and the application just goes back to the
Index method.
If a concurrency error is caught, the code redisplays the Delete confirmation page and provides a flag that
indicates it should display a concurrency error message.
Update the Delete view
In Views/Departments/Delete.cshtml, replace the scaffolded code with the following code that adds an error
message field and hidden fields for the DepartmentID and RowVersion properties. The changes are highlighted.
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>
<form asp-action="Delete">
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>
Run the app and go to the Departments Index page. Right-click the Delete hyperlink for the English department
and select Open in new tab, then in the first tab click the Edit hyperlink for the English department.
In the first window, change one of the values, and click Save:
In the second tab, click Delete. You see the concurrency error message, and the Department values are refreshed
with what's currently in the database.
If you click Delete again, you're redirected to the Index page, which shows that the department has been deleted.
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
Replace the code in Views/Departments/Create.cshtml to add a Select option to the drop-down list.
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Summary
This completes the introduction to handling concurrency conflicts. For more information about how to handle
concurrency in EF Core, see Concurrency conflicts. The next tutorial shows how to implement table-per-hierarchy
inheritance for the Instructor and Student entities.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Inheritance - 9 of
10
6/28/2018 • 8 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
In the previous tutorial, you handled concurrency exceptions. This tutorial will show you how to implement
inheritance in the data model.
In object-oriented programming, you can use inheritance to facilitate code reuse. In this tutorial, you'll change the
Instructor and Student classes so that they derive from a Person base class which contains properties such as
LastName that are common to both instructors and students. You won't add or change any web pages, but you'll
change some of the code and those changes will be automatically reflected in the database.
Suppose you want to eliminate the redundant code for the properties that are shared by the Instructor and
Student entities. Or you want to write a service that can format names without caring whether the name came
from an instructor or a student. You could create a Person base class that contains only those shared properties,
then make the Instructor and Student classes inherit from that base class, as shown in the following
illustration:
There are several ways this inheritance structure could be represented in the database. You could have a Person
table that includes information about both students and instructors in a single table. Some of the columns could
apply only to instructors (HireDate), some only to students (EnrollmentDate), some to both (LastName,
FirstName). Typically, you'd have a discriminator column to indicate which type each row represents. For
example, the discriminator column might have "Instructor" for instructors and "Student" for students.
This pattern of generating an entity inheritance structure from a single database table is called table-per-
hierarchy (TPH) inheritance.
An alternative is to make the database look more like the inheritance structure. For example, you could have only
the name fields in the Person table and have separate Instructor and Student tables with the date fields.
This pattern of making a database table for each entity class is called table per type (TPT) inheritance.
Yet another option is to map all non-abstract types to individual tables. All properties of a class, including
inherited properties, map to columns of the corresponding table. This pattern is called Table-per-Concrete Class
(TPC ) inheritance. If you implemented TPC inheritance for the Person, Student, and Instructor classes as shown
earlier, the Student and Instructor tables would look no different after implementing inheritance than they did
before.
TPC and TPH inheritance patterns generally deliver better performance than TPT inheritance patterns, because
TPT patterns can result in complex join queries.
This tutorial demonstrates how to implement TPH inheritance. TPH is the only inheritance pattern that the Entity
Framework Core supports. What you'll do is create a Person class, change the Instructor and Student classes
to derive from Person , add the new class to the DbContext , and create a migration.
TIP
Consider saving a copy of the project before making the following changes. Then if you run into problems and need to
start over, it will be easier to start from the saved project instead of reversing steps done for this tutorial or going back to
the beginning of the whole series.
namespace ContosoUniversity.Models
{
public abstract class Person
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
This is all that the Entity Framework needs in order to configure table-per-hierarchy inheritance. As you'll see,
when the database is updated, it will have a Person table in place of the Student and Instructor tables.
Create and customize migration code
Save your changes and build the project. Then open the command window in the project folder and enter the
following command:
Don't run the database update command yet. That command will result in lost data because it will drop the
Instructor table and rename the Student table to Person. You need to provide custom code to preserve existing
data.
Open Migrations/<timestamp>_Inheritance.cs and replace the Up method with the following code:
migrationBuilder.DropTable(
name: "Student");
migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");
migrationBuilder.AddForeignKey(
name: "FK_Enrollment_Person_StudentID",
table: "Enrollment",
column: "StudentID",
principalTable: "Person",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
}
(In a production system you would make corresponding changes to the Down method in case you ever had to
use that to go back to the previous database version. For this tutorial you won't be using the Down method.)
NOTE
It's possible to get other errors when making schema changes in a database that has existing data. If you get migration
errors that you can't resolve, you can either change the database name in the connection string or delete the database.
With a new database, there's no data to migrate, and the update-database command is more likely to complete without
errors. To delete the database, use SSOX or run the database drop CLI command.
Summary
You've implemented table-per-hierarchy inheritance for the Person , Student , and Instructor classes. For more
information about inheritance in Entity Framework Core, see Inheritance. In the next tutorial you'll see how to
handle a variety of relatively advanced Entity Framework scenarios.
P R E V IO U S NEXT
ASP.NET Core MVC with EF Core - Advanced - 10 of
10
9/18/2018 • 13 minutes to read • Edit Online
This tutorial has not been upgraded to ASP.NET Core 2.1. The ASP.NET Core 2.0 version of this tutorial is
available by selecting ASP.NET Core 2.0 above the table of contents or at the top of the page:
The ASP.NET Core 2.1 Razor Pages version of this tutorial has many improvements over the 2.0 version.
The 2.0 tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages
is a page-based programming model that makes building web UI easier and more productive. We recommend
the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow. For example, the scaffolding code has been significantly simplified.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub discussion.
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web
applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the
first tutorial in the series.
In the previous tutorial, you implemented table-per-hierarchy inheritance. This tutorial introduces several topics
that are useful to be aware of when you go beyond the basics of developing ASP.NET Core web applications that
use Entity Framework Core.
In DepartmentsController.cs, in the Details method, replace the code that retrieves a department with a
FromSql method call, as shown in the following highlighted code:
if (department == null)
{
return NotFound();
}
return View(department);
}
To verify that the new code works correctly, select the Departments tab and then Details for one of the
departments.
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount
= reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}
using System.Data.Common;
Run the app and go to the About page. It displays the same data it did before.
Call an update query
Suppose Contoso University administrators want to perform global changes in the database, such as changing
the number of credits for every course. If the university has a large number of courses, it would be inefficient to
retrieve them all as entities and change them individually. In this section you'll implement a web page that
enables the user to specify a factor by which to change the number of credits for all courses, and you'll make the
change by executing a SQL UPDATE statement. The web page will look like the following illustration:
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}
When the controller processes an HttpGet request, nothing is returned in ViewData["RowsAffected"] , and the view
displays an empty text box and a submit button, as shown in the preceding illustration.
When the Update button is clicked, the HttpPost method is called, and multiplier has the value entered in the text
box. The code then executes the SQL that updates courses and returns the number of affected rows to the view in
ViewData . When the view gets a RowsAffected value, it displays the number of rows updated.
In Solution Explorer, right-click the Views/Courses folder, and then click Add > New Item.
In the Add New Item dialog, click ASP.NET under Installed in the left pane, click MVC View Page, and name
the new view UpdateCourseCredits.cshtml.
In Views/Courses/UpdateCourseCredits.cshtml, replace the template code with the following code:
@{
ViewBag.Title = "UpdateCourseCredits";
}
Run the UpdateCourseCredits method by selecting the Courses tab, then adding "/UpdateCourseCredits" to the
end of the URL in the browser's address bar (for example: http://localhost:5813/Courses/UpdateCourseCredits ).
Enter a number in the text box:
You'll notice something here that might surprise you: the SQL selects up to 2 rows ( TOP(2) ) from the Person
table. The SingleOrDefaultAsync method doesn't resolve to 1 row on the server. Here's why:
If the query would return multiple rows, the method returns null.
To determine whether the query would return multiple rows, EF has to check if it returns at least 2.
Note that you don't have to use debug mode and stop at a breakpoint to get logging output in the Output
window. It's just a convenient way to stop the logging at the point you want to look at the output. If you don't do
that, logging continues and you have to scroll back to find the parts you're interested in.
_context.ChangeTracker.AutoDetectChangesEnabled = false;
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}
int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
page ?? 1, pageSize));
}
Next steps
This completes this series of tutorials on using the Entity Framework Core in an ASP.NET Core MVC application.
For more information about EF Core, see the Entity Framework Core documentation. A book is also available:
Entity Framework Core in Action.
For information on how to deploy a web app, see Host and deploy.
For information about other topics related to ASP.NET Core MVC, such as authentication and authorization, see
the ASP.NET Core documentation.
Acknowledgments
Tom Dykstra and Rick Anderson (twitter @RickAndMSFT) wrote this tutorial. Rowan Miller, Diego Vega, and
other members of the Entity Framework team assisted with code reviews and helped debug issues that arose
while we were writing code for the tutorials.
Common errors
ContosoUniversity.dll used by another process
Error message:
Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the
file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process.
Solution:
Stop the site in IIS Express. Go to the Windows System Tray, find IIS Express and right-click its icon, select the
Contoso University site, and then click Stop Site.
Migration scaffolded with no code in Up and Down methods
Possible cause:
The EF CLI commands don't automatically close and save code files. If you have unsaved changes when you run
the migrations add command, EF won't find your changes.
Solution:
Run the migrations remove command, save your code changes and rerun the migrations add command.
Errors while running database update
It's possible to get other errors when making schema changes in a database that has existing data. If you get
migration errors you can't resolve, you can either change the database name in the connection string or delete the
database. With a new database, there's no data to migrate, and the update-database command is much more
likely to complete without errors.
The simplest approach is to rename the database in appsettings.json. The next time you run database update ,a
new database will be created.
To delete a database in SSOX, right-click the database, click Delete, and then in the Delete Database dialog box
select Close existing connections and click OK.
To delete a database by using the CLI, run the database drop CLI command:
A network-related or instance-specific error occurred while establishing a connection to SQL Server. The
server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is
configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating
Server/Instance Specified)
Solution:
Check the connection string. If you have manually deleted the database file, change the name of the database in
the construction string to start over with a new database.
P R E V IO U S
ASP.NET Core tutorials
6/21/2018 • 2 minutes to read • Edit Online
The following step-by-step guides for developing ASP.NET Core applications are available:
By Rick Anderson
This tutorial teaches the basics of building an ASP.NET Core Razor Pages web app. We recommend you review
Introduction to Razor Pages before starting this tutorial. Razor Pages is the recommended way to build UI for web
applications in ASP.NET Core.
Prerequisites
Visual Studio for Mac
The preceding commands use the .NET Core CLI to create and run a Razor Pages project. Open a browser to
http://localhost:5000 to view the application.
The default template creates RazorPagesMovie, Home, About and Contact links and pages. Depending on the
size of your browser window, you might need to click the navigation icon to show the links.
Test the links. The RazorPagesMovie and Home links go to the Index page. The About and Contact links go to
the About and Contact pages, respectively.
appsettings.json Configuration
N E X T: A D D IN G A
M ODEL
Add a model to an ASP.NET Core Razor Pages app
with Visual Studio for Mac
6/21/2018 • 4 minutes to read • Edit Online
By Rick Anderson
In this section, you add classes for managing movies in a database. You use these classes 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 you create are known as POCO classes (from "plain-old CLR objects") because they don't have
any dependency on EF Core. They define the properties of the data that are stored in the database.
In this tutorial, you write the model classes first, and EF Core creates the database. An alternate approach not
covered here is to generate model classes from an existing database.
View or download sample.
using System;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet property for the entity set. In Entity Framework terminology, an entity set
typically corresponds to a database table, and an entity corresponds to a row in the table.
Add a database connection string
Add a connection string to the appsettings.json file.
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "Data Source=MvcMovie.db"
}
}
Right click on a red squiggly line, for example MovieContextin the line
services.AddDbContext<MovieContext>(options => . Select Quick Fix > using RazorPagesMovie.Models;. Visual
studio adds the using statement.
Build the project to verify you don't have any errors.
Entity Framework Core NuGet packages for migrations
The EF tools for the command-line interface (CLI) are provided in Microsoft.EntityFrameworkCore.Tools.DotNet.
Click on the Microsoft.EntityFrameworkCore.Tools.DotNet link to get the version number to use. To install this
package, add it to the DotNetCliToolReference collection in the .csproj file. Note: You have to install this package
by editing the .csproj file; you can't use the install-package command or the package manager GUI.
To edit a .csproj file:
Select File > Open, and then select the .csproj file.
Select Options.
Change Open with to Source Code Editor.
The version numbers shown in the following code were correct at the time of writing.
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-
final"/>
</ItemGroup>
From the command line, run the following .NET Core CLI commands:
The DotNetCliToolReference element and the add package command install the tooling required to run the
scaffolding engine.
The ef migrations add InitialCreate command generates code to create the initial database schema. The schema
is based on the model specified in the DbContext (In the Models/MovieContext.cs file). The InitialCreate
argument is used to name the migrations. You can use any name, but by convention you choose a name that
describes the migration. See Introduction to migrations for more information.
The ef database update command runs the Up method in the Migrations/<time-stamp>_InitialCreate.cs file,
which creates the database.
Scaffold the Movie model
Run the following from the command line (in the project directory that contains the Program.cs, Startup.cs,
and .csproj files):
Open a command shell to the project directory (The directory that contains the Program.cs, Startup.cs, and .csproj
files).
If you get the error:
PARAMETER DESCRIPTION
P R E V IO U S : G E T N E X T: S C A F F O L D E D R A Z O R
STA RTE D PAGES
Scaffolded Razor Pages in ASP.NET Core
6/21/2018 • 8 minutes to read • Edit Online
By Rick Anderson
This tutorial examines the Razor Pages created by scaffolding in the previous tutorial.
View or download sample.
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
Razor Pages are derived from PageModel . By convention, the PageModel -derived class is called <PageName>Model .
The constructor uses dependency injection to add the MovieContext to the page. All the scaffolded pages follow
this pattern. See Asynchronous code for more information on asynchronous programing with Entity Framework.
When a request is made for the page, the OnGetAsync method returns a list of movies to the Razor Page.
OnGetAsync or OnGet is called on a Razor Page to initialize the state for the page. In this case, OnGetAsync gets a
list of movies and displays them.
When OnGet returns void or OnGetAsync returns Task , no return method is used. When the return type is
IActionResult or Task<IActionResult> , a return statement must be provided. For example, the
Pages/Movies/Create.cshtml.cs OnPostAsync method:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Razor can transition from HTML into C# or into Razor-specific markup. When an @ symbol is followed by a
Razor reserved keyword, it transitions into Razor-specific markup, otherwise it transitions into C#.
The @page Razor directive makes the file into an MVC action — which means that it can handle requests. @page
must be the first Razor directive on a page. @page is an example of transitioning into Razor-specific markup. See
Razor syntax for more information.
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.Movie[0].Title))
The DisplayNameFor HTML Helper inspects the Title property referenced in the lambda expression to determine
the display name. The lambda expression is inspected rather than evaluated. That means there is no access
violation when model , model.Movie , or model.Movie[0] are null or empty. When the lambda expression is
evaluated (for example, with @Html.DisplayFor(modelItem => item.Title) ), the model's property values are
evaluated.
The @model directive
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
The directive specifies the type of the model passed to the Razor Page. In the preceding example, the
@model
@model line makes the PageModel -derived class available to the Razor Page. The model is used in the
@Html.DisplayNameFor and @Html.DisplayName HTML Helpers on the page.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
The preceding highlighted code is an example of Razor transitioning into C#. The { and } characters enclose a
block of C# code.
The PageModel base class has a ViewData dictionary property that can be used to add data that you want to pass
to a View. You add objects into the ViewData dictionary using a key/value pattern. In the preceding sample, the
"Title" property is added to the ViewData dictionary.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the Pages/Shared/_Layout.cshtml file.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the _Layout.cshtml file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
The line @*Markup removed for brevity.*@ is a Razor comment. Unlike HTML comments ( <!-- --> ), Razor
comments are not sent to the client.
Run the app and test the links in the project (Home, About, Contact, Create, Edit, and Delete). Each page sets
the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.
Pages/Index.cshtml and Pages/Movies/Index.cshtml currently have the same title, but you can modify them to have
different values.
NOTE
You may not be able to enter decimal commas in the Price field. To support jQuery validation for non-English locales that
use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. This
GitHub issue 4076 for instructions on adding decimal comma.
@{
Layout = "_Layout";
}
The preceding markup sets the layout file to Pages/Shared/_Layout.cshtml for all Razor files under the Pages
folder. See Layout for more information.
Update the layout
Change the <title> element in the Pages/Shared/_Layout.cshtml file to use a shorter string.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>
The preceding anchor element is a Tag Helper. In this case, it's the Anchor Tag Helper. The
asp-page="/Movies/Index" Tag Helper attribute and value creates a link to the /Movies/Index Razor Page.
Save your changes, and test the app by clicking on the RpMovie link. See the _Layout.cshtml file in GitHub.
The Create page model
Examine the Pages/Movies/Create.cshtml.cs page model:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
The method initializes any state needed for the page. The Create page doesn't have any state to initialize, so
OnGet
Page is returned. Later in the tutorial you see OnGet method initialize state. The Page method creates a
PageResult object that renders the Create.cshtml page.
The Movie property uses the [BindProperty] attribute to opt-in to model binding. When the Create form posts
the form values, the ASP.NET Core runtime binds the posted values to the Movie model.
The OnPostAsync method is run when the page posts form data:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be
caught on the client-side before the form is posted. An example of a model error is posting a value for the date
field that cannot be converted to a date. We'll talk more about client-side validation and model validation later in
the tutorial.
If there are no model errors, the data is saved, and the browser is redirected to the Index page.
The Create Razor Page
Examine the Pages/Movies/Create.cshtml Razor Page file:
@page
@model RazorPagesMovie.Pages.Movies.CreateModel
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<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-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
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:
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
The Validation Tag Helpers ( <div asp-validation-summary and <span asp-validation-for ) display validation errors.
Validation is covered in more detail later in this series.
The Label Tag Helper ( <label asp-for="Movie.Title" class="control-label"></label> ) generates the label caption
and for attribute for the Title property.
The Input Tag Helper ( <input asp-for="Movie.Title" class="form-control" /> ) uses the DataAnnotations attributes
and produces HTML attributes needed for jQuery Validation on the client-side.
The next tutorial explains SQLite and seeding the database.
P R E V IO U S : A D D IN G A N E X T:
M ODEL S Q L IT E
Work with SQLite in an ASP.NET Core Razor Pages
app
6/26/2018 • 2 minutes to read • Edit Online
By Rick Anderson
The MovieContext object handles the task of connecting to the database and mapping Movie objects to database
records. The database context is registered with the Dependency Injection container in the ConfigureServices
method in the Startup.cs file:
services.AddDbContext<MovieContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}
SQLite
The SQLite website states:
There are many third party tools you can download to manage and view a SQLite database. The image below is
from DB Browser for SQLite. If you have a favorite SQLite tool, leave a comment on what you like about it.
Seed the database
Create a new class named SeedData in the Models folder. Replace the generated code with the following:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the DB, the seed initializer returns.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
try
{
var context = services.GetRequiredService<MovieContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal. We don't want to see the time (12:00:00
AM in the image below ) and ReleaseDate should be Release Date (two words).
using System;
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly
map Price to currency in the database. For more information, see Data Types.
The completed model:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
We'll cover DataAnnotations in the next tutorial. The Display attribute specifies what to display for the name of a
field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data
(Date), so the time information stored in the field isn't displayed.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml file.
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
preceding code, the AnchorTagHelper dynamically generates the HTML href attribute value from the Razor Page
(the route is relative), the asp-page , and the route id ( asp-route-id ). See URL generation for Pages for more
information.
Use View Source from your favorite browser to examine the generated markup. A portion of the generated
HTML is shown below:
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>
The dynamically-generated links pass the movie ID with a query string (for example,
http://localhost:5000/Movies/Details?id=2 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive for
each of these pages from @page to @page "{id:int}" . Run the app and then view source. The generated HTML
adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the "{id:int}" route template that does not include the integer will return an HTTP 404
(not found) error. For example, http://localhost:5000/Movies/Details will return a 404 error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code only detects concurrency exceptions when the first concurrent client deletes the movie, and the
second concurrent client posts changes to the movie.
To test the catch block:
Set a breakpoint on catch (DbUpdateConcurrencyException)
Edit a movie.
In another browser window, select the Delete link for the same movie, and then delete the movie.
In the previous browser window, post changes to the movie.
Production code would generally detect concurrency conflicts when two or more clients concurrently updated a
record. See Handle concurrency conflicts for more information.
Posting and binding review
Examine the Pages/Movies/Edit.cshtml.cs file:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
When an HTTP GET request is made to the Movies/Edit page (for example, http://localhost:5000/Movies/Edit/2 ):
The OnGetAsync method fetches the movie from the database and returns the Page method.
The Page method renders the Pages/Movies/Edit.cshtml Razor Page. The Pages/Movies/Edit.cshtml file
contains the model directive ( @model RazorPagesMovie.Pages.Movies.EditModel ), which makes the movie model
available on the page.
The Edit form is displayed with the values from the movie.
When the Movies/Edit page is posted:
The form values on the page are bound to the Movie property. The [BindProperty] attribute enables
Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state (for example, ReleaseDate cannot be converted to a date), the form is
posted again with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST
OnPostAsync method in the Create Razor Page follows a similar pattern to the OnPostAsync method in the Edit
Razor Page.
Search is added in the next tutorial.
P R E V IO U S : W O R K W IT H ADD
S Q L L IT E SE A RCH
Add search to an ASP.NET Core Razor Pages app
6/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
In this document, search capability is added to the Index page that enables searching movies by genre or name.
Update the Index page's OnGetAsync method with the following code:
@{
Layout = "_Layout";
}
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the OnGetAsync method creates a LINQ query to select the movies:
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 search string:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code is a Lambda Expression. Lambdas are used in method-based LINQ queries as
arguments to standard query operator methods such as the Where method or Contains (used in the preceding
code). LINQ queries are not executed when they're defined or when they're modified by calling a method (such as
Where , Contains or OrderBy ). Rather, query execution is deferred. That means the evaluation of an expression is
delayed until its realized value is iterated over or the ToListAsync method is called. See Query Execution for more
information.
Note: The Contains method is run on the database, not in the C# code. The case sensitivity on the query depends
on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case insensitive. In
SQLite, with the default collation, it's case sensitive.
Navigate to the Movies page and append a query string such as ?searchString=Ghost to the URL (for example,
http://localhost:5000/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, http://localhost:5000/Movies/ghost ).
@page "{searchString?}"
The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query
string value. The ? in "{searchString?}" means this is an optional route parameter.
However, you can't expect users to modify the URL to search for a movie. In this step, UI is added to filter movies.
If you added the route constraint "{searchString?}" , remove it.
Open the Pages/Movies/Index.cshtml file, and add the <form> markup highlighted in the following code:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
The HTML <form> tag uses the Form Tag Helper. When the form is submitted, the filter string is sent to the
Pages/Movies/Index page. Save the changes and test the filter.
Search by genre
Add the following highlighted properties to Pages/Movies/Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
The SelectList Genres contains the list of genres. This allows the user to select a genre from the list.
The MovieGenre property contains the specific genre the user selects (for example, "Western").
Update the OnGetAsync method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}
The following code is a LINQ query that retrieves all the genres from the database.
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<table class="table">
<thead>
P R E V IO U S : U P D A T IN G T H E N E X T: A D D IN G A N E W
PAGES F IE L D
Create a Razor Pages web app with ASP.NET Core
and Visual Studio Code
6/21/2018 • 2 minutes to read • Edit Online
By Rick Anderson
This tutorial teaches the basics of building an ASP.NET Core Razor Pages web app. We recommend you complete
Introduction to Razor Pages before starting this tutorial. Razor Pages is the recommended way to build UI for web
applications in ASP.NET Core.
Prerequisites
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code
The preceding commands use the .NET Core CLI to create and run a Razor Pages project. Open a browser to
http://localhost:5000 to view the application.
The default template creates RazorPagesMovie, Home, About and Contact links and pages. Depending on the
size of your browser window, you might need to click the navigation icon to show the links.
Test the links. The RazorPagesMovie and Home links go to the Index page. The About and Contact links go to
the About and Contact pages, respectively.
appsettings.json Configuration
By Rick Anderson
In this section, you add classes for managing movies in a database. You use these classes 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 you create are known as POCO classes (from "plain-old CLR objects") because they don't have
any dependency on EF Core. They define the properties of the data that are stored in the database.
In this tutorial, you write the model classes first, and EF Core creates the database. An alternate approach not
covered here is to generate model classes from an existing database.
View or download sample.
using System;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}
The preceding code creates a DbSet property for the entity set. In Entity Framework terminology, an entity set
typically corresponds to a database table, and an entity corresponds to a row in the table.
Add a database connection string
Add a connection string to the appsettings.json file.
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"MovieContext": "Data Source=MvcMovie.db"
}
}
using RazorPagesMovie.Models;
using Microsoft.EntityFrameworkCore;
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.2" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
</ItemGroup>
</Project>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-
final"/>
</ItemGroup>
From the command line, run the following .NET Core CLI commands:
The DotNetCliToolReference element and the add package command install the tooling required to run the
scaffolding engine.
The ef migrations add InitialCreate command generates code to create the initial database schema. The schema
is based on the model specified in the DbContext (In the Models/MovieContext.cs file). The InitialCreate
argument is used to name the migrations. You can use any name, but by convention you choose a name that
describes the migration. See Introduction to migrations for more information.
The ef database update command runs the Up method in the Migrations/<time-stamp>_InitialCreate.cs file,
which creates the database.
Scaffold the Movie model
Open a command window in the project directory (The directory that contains the Program.cs, Startup.cs, and
.csproj files).
Run the following command:
Note: Run the following command on Windows. For MacOS and Linux, see the next command
dotnet aspnet-codegenerator razorpage -m Movie -dc MovieContext -udl -outDir Pages\Movies --
referenceScriptLibraries
PARAMETER DESCRIPTION
P R E V IO U S : G E T N E X T: S C A F F O L D E D R A Z O R
STA RTE D PAGES
Scaffolded Razor Pages in ASP.NET Core
6/21/2018 • 8 minutes to read • Edit Online
By Rick Anderson
This tutorial examines the Razor Pages created by scaffolding in the previous tutorial.
View or download sample.
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
Razor Pages are derived from PageModel . By convention, the PageModel -derived class is called <PageName>Model .
The constructor uses dependency injection to add the MovieContext to the page. All the scaffolded pages follow
this pattern. See Asynchronous code for more information on asynchronous programing with Entity Framework.
When a request is made for the page, the OnGetAsync method returns a list of movies to the Razor Page.
OnGetAsync or OnGet is called on a Razor Page to initialize the state for the page. In this case, OnGetAsync gets a
list of movies and displays them.
When OnGet returns void or OnGetAsync returns Task , no return method is used. When the return type is
IActionResult or Task<IActionResult> , a return statement must be provided. For example, the
Pages/Movies/Create.cshtml.cs OnPostAsync method:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movie[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Razor can transition from HTML into C# or into Razor-specific markup. When an @ symbol is followed by a
Razor reserved keyword, it transitions into Razor-specific markup, otherwise it transitions into C#.
The @page Razor directive makes the file into an MVC action — which means that it can handle requests. @page
must be the first Razor directive on a page. @page is an example of transitioning into Razor-specific markup. See
Razor syntax for more information.
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.Movie[0].Title))
The DisplayNameFor HTML Helper inspects the Title property referenced in the lambda expression to determine
the display name. The lambda expression is inspected rather than evaluated. That means there is no access
violation when model , model.Movie , or model.Movie[0] are null or empty. When the lambda expression is
evaluated (for example, with @Html.DisplayFor(modelItem => item.Title) ), the model's property values are
evaluated.
The @model directive
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
The directive specifies the type of the model passed to the Razor Page. In the preceding example, the
@model
@model line makes the PageModel -derived class available to the Razor Page. The model is used in the
@Html.DisplayNameFor and @Html.DisplayName HTML Helpers on the page.
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
The preceding highlighted code is an example of Razor transitioning into C#. The { and } characters enclose a
block of C# code.
The PageModel base class has a ViewData dictionary property that can be used to add data that you want to pass
to a View. You add objects into the ViewData dictionary using a key/value pattern. In the preceding sample, the
"Title" property is added to the ViewData dictionary.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the Pages/Shared/_Layout.cshtml file.
The "Title" property is used in the Pages/Shared/_Layout.cshtml file. The following markup shows the first few
lines of the _Layout.cshtml file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
The line @*Markup removed for brevity.*@ is a Razor comment. Unlike HTML comments ( <!-- --> ), Razor
comments are not sent to the client.
Run the app and test the links in the project (Home, About, Contact, Create, Edit, and Delete). Each page sets
the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.
Pages/Index.cshtml and Pages/Movies/Index.cshtml currently have the same title, but you can modify them to have
different values.
NOTE
You may not be able to enter decimal commas in the Price field. To support jQuery validation for non-English locales that
use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. This
GitHub issue 4076 for instructions on adding decimal comma.
@{
Layout = "_Layout";
}
The preceding markup sets the layout file to Pages/Shared/_Layout.cshtml for all Razor files under the Pages
folder. See Layout for more information.
Update the layout
Change the <title> element in the Pages/Shared/_Layout.cshtml file to use a shorter string.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie</title>
The preceding anchor element is a Tag Helper. In this case, it's the Anchor Tag Helper. The
asp-page="/Movies/Index" Tag Helper attribute and value creates a link to the /Movies/Index Razor Page.
Save your changes, and test the app by clicking on the RpMovie link. See the _Layout.cshtml file in GitHub.
The Create page model
Examine the Pages/Movies/Create.cshtml.cs page model:
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
// Unused usings removed.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
The method initializes any state needed for the page. The Create page doesn't have any state to initialize, so
OnGet
Page is returned. Later in the tutorial you see OnGet method initialize state. The Page method creates a
PageResult object that renders the Create.cshtml page.
The Movie property uses the [BindProperty] attribute to opt-in to model binding. When the Create form posts
the form values, the ASP.NET Core runtime binds the posted values to the Movie model.
The OnPostAsync method is run when the page posts form data:
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be
caught on the client-side before the form is posted. An example of a model error is posting a value for the date
field that cannot be converted to a date. We'll talk more about client-side validation and model validation later in
the tutorial.
If there are no model errors, the data is saved, and the browser is redirected to the Index page.
The Create Razor Page
Examine the Pages/Movies/Create.cshtml Razor Page file:
@page
@model RazorPagesMovie.Pages.Movies.CreateModel
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<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-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
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:
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
The Validation Tag Helpers ( <div asp-validation-summary and <span asp-validation-for ) display validation errors.
Validation is covered in more detail later in this series.
The Label Tag Helper ( <label asp-for="Movie.Title" class="control-label"></label> ) generates the label caption
and for attribute for the Title property.
The Input Tag Helper ( <input asp-for="Movie.Title" class="form-control" /> ) uses the DataAnnotations attributes
and produces HTML attributes needed for jQuery Validation on the client-side.
The next tutorial explains SQLite and seeding the database.
P R E V IO U S : A D D IN G A N E X T:
M ODEL S Q L IT E
Work with SQLite in an ASP.NET Core Razor Pages
app
6/26/2018 • 2 minutes to read • Edit Online
By Rick Anderson
The MovieContext object handles the task of connecting to the database and mapping Movie objects to database
records. The database context is registered with the Dependency Injection container in the ConfigureServices
method in the Startup.cs file:
services.AddDbContext<MovieContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("MovieContext")));
services.AddMvc();
}
SQLite
The SQLite website states:
There are many third party tools you can download to manage and view a SQLite database. The image below is
from DB Browser for SQLite. If you have a favorite SQLite tool, leave a comment on what you like about it.
Seed the database
Create a new class named SeedData in the Models folder. Replace the generated code with the following:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
namespace RazorPagesMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the DB, the seed initializer returns.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
namespace RazorPagesMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
try
{
var context = services.GetRequiredService<MovieContext>();
// requires using Microsoft.EntityFrameworkCore;
context.Database.Migrate();
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal. We don't want to see the time (12:00:00
AM in the image below ) and ReleaseDate should be Release Date (two words).
using System;
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly
map Price to currency in the database. For more information, see Data Types.
The completed model:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
We'll cover DataAnnotations in the next tutorial. The Display attribute specifies what to display for the name of a
field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data
(Date), so the time information stored in the field isn't displayed.
Browse to Pages/Movies and hover over an Edit link to see the target URL.
The Edit, Details, and Delete links are generated by the Anchor Tag Helper in the Pages/Movies/Index.cshtml file.
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
preceding code, the AnchorTagHelper dynamically generates the HTML href attribute value from the Razor Page
(the route is relative), the asp-page , and the route id ( asp-route-id ). See URL generation for Pages for more
information.
Use View Source from your favorite browser to examine the generated markup. A portion of the generated
HTML is shown below:
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>
The dynamically-generated links pass the movie ID with a query string (for example,
http://localhost:5000/Movies/Details?id=2 ).
Update the Edit, Details, and Delete Razor Pages to use the "{id:int}" route template. Change the page directive for
each of these pages from @page to @page "{id:int}" . Run the app and then view source. The generated HTML
adds the ID to the path portion of the URL:
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
A request to the page with the "{id:int}" route template that does not include the integer will return an HTTP 404
(not found) error. For example, http://localhost:5000/Movies/Details will return a 404 error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
The previous code only detects concurrency exceptions when the first concurrent client deletes the movie, and the
second concurrent client posts changes to the movie.
To test the catch block:
Set a breakpoint on catch (DbUpdateConcurrencyException)
Edit a movie.
In another browser window, select the Delete link for the same movie, and then delete the movie.
In the previous browser window, post changes to the movie.
Production code would generally detect concurrency conflicts when two or more clients concurrently updated a
record. See Handle concurrency conflicts for more information.
Posting and binding review
Examine the Pages/Movies/Edit.cshtml.cs file:
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Movie Movie { get; set; }
if (Movie == null)
{
return NotFound();
}
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Movie.Any(e => e.ID == Movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
}
When an HTTP GET request is made to the Movies/Edit page (for example, http://localhost:5000/Movies/Edit/2 ):
The OnGetAsync method fetches the movie from the database and returns the Page method.
The Page method renders the Pages/Movies/Edit.cshtml Razor Page. The Pages/Movies/Edit.cshtml file
contains the model directive ( @model RazorPagesMovie.Pages.Movies.EditModel ), which makes the movie model
available on the page.
The Edit form is displayed with the values from the movie.
When the Movies/Edit page is posted:
The form values on the page are bound to the Movie property. The [BindProperty] attribute enables
Model binding.
[BindProperty]
public Movie Movie { get; set; }
If there are errors in the model state (for example, ReleaseDate cannot be converted to a date), the form is
posted again with the submitted values.
If there are no model errors, the movie is saved.
The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST
OnPostAsync method in the Create Razor Page follows a similar pattern to the OnPostAsync method in the Edit
Razor Page.
Search is added in the next tutorial.
P R E V IO U S : W O R K IN G W IT H ADD
S Q L L IT E SE A RCH
Add search to an ASP.NET Core Razor Pages app
6/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
In this document, search capability is added to the Index page that enables searching movies by genre or name.
Update the Index page's OnGetAsync method with the following code:
@{
Layout = "_Layout";
}
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the OnGetAsync method creates a LINQ query to select the movies:
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 search string:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code is a Lambda Expression. Lambdas are used in method-based LINQ queries as
arguments to standard query operator methods such as the Where method or Contains (used in the preceding
code). LINQ queries are not executed when they're defined or when they're modified by calling a method (such as
Where , Contains or OrderBy ). Rather, query execution is deferred. That means the evaluation of an expression is
delayed until its realized value is iterated over or the ToListAsync method is called. See Query Execution for more
information.
Note: The Contains method is run on the database, not in the C# code. The case sensitivity on the query depends
on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case insensitive. In
SQLite, with the default collation, it's case sensitive.
Navigate to the Movies page and append a query string such as ?searchString=Ghost to the URL (for example,
http://localhost:5000/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, http://localhost:5000/Movies/ghost ).
@page "{searchString?}"
The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query
string value. The ? in "{searchString?}" means this is an optional route parameter.
However, you can't expect users to modify the URL to search for a movie. In this step, UI is added to filter movies.
If you added the route constraint "{searchString?}" , remove it.
Open the Pages/Movies/Index.cshtml file, and add the <form> markup highlighted in the following code:
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
The HTML <form> tag uses the Form Tag Helper. When the form is submitted, the filter string is sent to the
Pages/Movies/Index page. Save the changes and test the filter.
Search by genre
Add the following highlighted properties to Pages/Movies/Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
The SelectList Genres contains the list of genres. This allows the user to select a genre from the list.
The MovieGenre property contains the specific genre the user selects (for example, "Western").
Update the OnGetAsync method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}
The following code is a LINQ query that retrieves all the genres from the database.
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<table class="table">
<thead>
P R E V IO U S : U P D A T IN G T H E N E X T: A D D IN G A N E W
PAGES F IE L D
Create a web app with ASP.NET Core MVC on
macOS with Visual Studio for Mac
6/21/2018 • 2 minutes to read • Edit Online
This series of tutorials teaches you the basics of building an ASP.NET Core MVC web app using Visual Studio for
Mac.
This tutorial teaches ASP.NET Core MVC web development with controllers and views. Razor Pages is a feature of
the ASP.NET Core MVC framework that makes building and testing web UI easier and more productive. You can
use Razor pages alongside controllers and views in the same project.
We recommend you try the Razor Pages tutorial before the MVC/Controller/Views version. The Razor Pages
tutorial:
Is the preferred approach for new application development.
Is easier to follow.
Covers more features.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
1. Get started
2. Add a controller
3. Add a view
4. Add a model
5. SQLite
6. Controller methods and views
7. Add search
8. Add a new field
9. Add validation
10. Examine the Details and Delete methods
Get started with ASP.NET Core MVC and Visual
Studio for Mac
6/21/2018 • 2 minutes to read • Edit Online
By Rick Anderson
This tutorial teaches you the basics of building an ASP.NET Core MVC web app using Visual Studio for Mac.
This tutorial teaches ASP.NET Core MVC web development with controllers and views. Razor Pages is a feature of
the ASP.NET Core MVC framework that makes building and testing web UI easier and more productive. You can
use Razor pages alongside controllers and views in the same project.
We recommend you try the Razor Pages tutorial before the MVC/Controller/Views version. The Razor Pages
tutorial:
Is the preferred approach for new application development.
Is easier to follow.
Covers more features.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
There are 3 versions of this tutorial:
macOS: Build an ASP.NET Core MVC app with Visual Studio for Mac
Windows: Build an ASP.NET Core MVC app with Visual Studio
Linux, macOS, and Windows: Build an ASP.NET Core MVC app with Visual Studio Code
Prerequisites
Visual Studio for Mac
The address bar shows localhost:port# and not something like example.com . That's because localhost is the
standard hostname for your local computer. When Visual Studio creates a web project, a random port is used
for the web server. When you run the app, you'll see a different port number.
You can launch the app in debug or non-debug mode from the Run menu.
The default template gives you Home, About and Contact links. The browser image above doesn't show these
links. Depending on the size of your browser, you might need to click the navigation icon to show them.
In the next part of this tutorial, you learn about MVC and start writing some code.
NEXT
Add a controller to an ASP.NET Core MVC app with
Visual Studio for Mac
6/21/2018 • 5 minutes to read • Edit Online
By Rick Anderson
The Model-View -Controller (MVC ) architectural pattern separates an app into three main components: Model,
View, and Controller. The MVC pattern helps you create apps that are more testable and easier to update than
traditional monolithic apps. MVC -based apps contain:
Models: Classes that represent the data of the app. The model classes use validation logic to enforce
business rules for that data. Typically, model objects retrieve and store model state in a database. In this
tutorial, a Movie model retrieves movie data from a database, provides it to the view or updates it. Updated
data is written to a database.
Views: Views are the components that display the app's user interface (UI). Generally, this UI displays the
model data.
Controllers: Classes that handle browser requests. They retrieve model data and call view templates that
return a response. In an MVC app, the view only displays information; the controller handles and responds
to user input and interaction. For example, the controller handles route data and query-string values, and
passes these values to the model. The model might use these values to query the database. For example,
http://localhost:1234/Home/About has route data of Home (the controller ) and About (the action method to
call on the home controller). http://localhost:1234/Movies/Edit/5 is a request to edit the movie with ID=5
using the movie controller. We'll talk about route data later in the tutorial.
The MVC pattern helps you create apps that separate the different aspects of the app (input logic, business logic,
and UI logic), while providing a loose coupling between these elements. The pattern specifies where each kind of
logic should be located in the app. The UI logic belongs in the view. Input logic belongs in the controller. Business
logic belongs in the model. This separation helps you manage complexity when you build an app, because it
enables you to work on one aspect of the implementation at a time without impacting the code of another. For
example, you can work on the view code without depending on the business logic code.
We cover these concepts in this tutorial series and show you how to use them to build a movie app. The MVC
project contains folders for the Controllers and Views.
Add a controller
In Solution Explorer, right-click Controllers > Add > New File.
Select ASP.NET Core and MVC Controller Class.
Name the controller HelloWorldController.
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
//
// GET: /HelloWorld/Welcome/
Every public method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a
string. Note the comments preceding each method.
An HTTP endpoint is a targetable URL in the web application, such as http://localhost:1234/HelloWorld , and
combines the protocol used: HTTP , the network location of the web server (including the TCP port):
localhost:1234 and the target URI HelloWorld .
The first comment states this is an HTTP GET method that's invoked by appending "/HelloWorld/" to the base
URL. The second comment specifies an HTTP GET method that's invoked by appending "/HelloWorld/Welcome/"
to the URL. Later on in the tutorial you'll use the scaffolding engine to generate HTTP POST methods.
Run the app in non-debug mode and append "HelloWorld" to the path in the address bar. The Index method
returns a string.
MVC invokes controller classes (and the action methods within them) depending on the incoming URL. The
default URL routing logic used by MVC uses a format like this to determine what code to invoke:
/[Controller]/[ActionName]/[Parameters]
You set the format for routing in the Configure method in Startup.cs file.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
When you run the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index"
method specified in the template line highlighted above.
The first URL segment determines the controller class to run. So localhost:xxxx/HelloWorld maps to the
HelloWorldController class. The second part of the URL segment determines the action method on the class. So
localhost:xxxx/HelloWorld/Index would cause the Index method of the HelloWorldController class to run. Notice
that you only had to browse to localhost:xxxx/HelloWorld and the Index method was called by default. This is
because Index is the default method that will be called on a controller if a method name isn't explicitly specified.
The third part of the URL segment ( id ) is for route data. You'll see route data later on in this tutorial.
Browse to http://localhost:xxxx/HelloWorld/Welcome . The Welcome method runs and returns the string "This is the
Welcome action method...". For this URL, the controller is HelloWorld and Welcome is the action method. You
haven't used the [Parameters] part of the URL yet.
Modify the code to pass some parameter information from the URL to the controller. For example,
/HelloWorld/Welcome?name=Rick&numtimes=4 . Change the Welcome method to include two parameters as shown in
the following code.
// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
(Replace xxxx with your port number.) You can try different values for name and numtimes in the URL. The MVC
model binding system automatically maps the named parameters from the query string in the address bar to
parameters in your method. See Model Binding for more information.
In the image above, the URL segment ( Parameters ) isn't used, the name and numTimes parameters are passed as
query strings. The ? (question mark) in the above URL is a separator, and the query strings follow. The &
character separates query strings.
Replace the Welcome method with the following code:
This time the third URL segment matched the route parameter id . The Welcome method contains a parameter
id that matched the URL template in the MapRoute method. The trailing ? (in id? ) indicates the id parameter
is optional.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
In these examples the controller has been doing the "VC" portion of MVC - that is, the view and controller work.
The controller is returning HTML directly. Generally you don't want controllers returning HTML directly, since that
becomes very cumbersome to code and maintain. Instead you typically use a separate Razor view template file to
help generate the HTML response. You do that in the next tutorial.
P R E V IO U S NEXT
Add a view to an ASP.NET Core MVC app
6/21/2018 • 8 minutes to read • Edit Online
By Rick Anderson
In this section you modify the HelloWorldController class to use Razor view template files to cleanly encapsulate
the process of generating HTML responses to a client.
You create a view template file using Razor. Razor-based view templates have a .cshtml file extension. They
provide an elegant way to create HTML output using C#.
Currently the method returns a string with a message that's hard-coded in the controller class. In the
Index
HelloWorldController class, replace the Index method with the following code:
The preceding code returns a View object. It uses a view template to generate an HTML response to the browser.
Controller methods (also known as action methods) such as the Index method above, generally return an
IActionResult (or a class derived from ActionResult ), not a type like string.
Add a view
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 File.
In the New File dialog:
Select Web in the left pane.
Select Empty HTML file in the center pane.
Type Index.cshtml in the Name box.
Select New.
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the following:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
If your browser window is small (for example on a mobile device), you might need to toggle (tap) the Bootstrap
navigation button in the upper right to see the Home, About, and Contact links.
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Movies" asp-action="Index" class="navbar-brand">Movie App</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - MvcMovie</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Movies" asp-action="Index" class="navbar-brand">Movie App</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<footer>
<p>© 2017 - MvcMovie</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
WARNING
We haven't implemented the Movies controller yet, so if you click on that link, you'll get a 404 (Not found) error.
Save your changes and tap the About link. Notice how the title on the browser tab now displays About - Movie
App instead of About - Mvc Movie:
Tap the Contact link and notice that the title and anchor text also display Movie App. We were able to make the
change once in the layout template and have all pages on the site reflect the new link text and new title.
Examine the Views/_ViewStart.cshtml file:
@{
Layout = "_Layout";
}
The Views/_ViewStart.cshtml file brings in the Views/Shared/_Layout.cshtml file to each view. You can use the
Layout property to set a different layout view, or set it to null so no layout file will be used.
You'll make them slightly different so you can see which bit of code changes which part of the app.
@{
ViewData["Title"] = "Movie List";
}
ViewData["Title"] = "Movie List"; in the code above sets the Title property of the ViewData dictionary to
"Movie List". The Title property is used in the <title> HTML element in the layout page:
<title>@ViewData["Title"] - Movie App</title>
Save your change and navigate to http://localhost:xxxx/HelloWorld . Notice that the browser title, the primary
heading, and the secondary headings have changed. (If you don't see changes in the browser, you might be
viewing cached content. Press Ctrl+F5 in your browser to force the response from the server to be loaded.) The
browser title is created with ViewData["Title"] we set in the Index.cshtml view template and the additional "-
Movie App" added in the layout file.
Also notice how the content in the Index.cshtml view template was merged with the Views/Shared/_Layout.cshtml
view template and a single HTML response was sent to the browser. Layout templates make it really easy to make
changes that apply across all of the pages in your application. To learn more see Layout.
Our little bit of "data" (in this case the "Hello from our View Template!" message) is hard-coded, though. The MVC
application has a "V" (view ) and you've got a "C" (controller), but no "M" (model) yet.
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}
return View();
}
}
}
The ViewData dictionary object contains data that will be passed to the view.
Create a Welcome view template named Views/HelloWorld/Welcome.cshtml.
You'll create a loop in the Welcome.cshtml view template that displays "Hello" NumTimes . Replace the contents of
Views/HelloWorld/Welcome.cshtml with the following:
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
Data is taken from the URL and passed to the controller using the MVC model binder . The controller packages
the data into a ViewData dictionary and passes that object to the view. The view then renders the data as HTML to
the browser.
In the sample above, we used the ViewData dictionary to pass data from the controller to a view. Later in the
tutorial, we will use a view model to pass data from a controller to a view. The view model approach to passing
data is generally much preferred over the ViewData dictionary approach. See ViewModel vs ViewData vs
ViewBag vs TempData vs Session in MVC for more information.
Well, that was a kind of an "M" for model, but not the database kind. Let's take what we've learned and create a
database of movies.
P R E V IO U S NEXT
Add a model to an ASP.NET Core MVC app
6/26/2018 • 5 minutes to read • Edit Online
using System;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0"
/>
</ItemGroup>
</Project>
using Microsoft.EntityFrameworkCore;
namespace MvcMovie.Models
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
namespace MvcMovie
{
public class Startup
{
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));
This tells Entity Framework which model classes are included in the data model. You're defining one entity
set of Movie objects, which will be represented in the database as a Movie table.
Build the project to verify there are no errors.
dotnet restore
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --
relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries
If you get the error No executable found matching command "dotnet-aspnet-codegenerator", verify :
You are in the project directory. The project directory has the Program.cs, Startup.cs and .csproj files.
Your dotnet version is 1.1 or higher. Run dotnet to get the version.
You have added the <DotNetCliToolReference> element to the MvcMovie.csproj file.
The dotnet ef migrations add InitialCreate command generates code to create the initial database schema. The
schema is based on the model specified in the DbContext (In the Models/MvcMovieContext.cs file). The Initial
argument is used to name the migrations. You can use any name, but by convention you choose a name that
describes the migration. See Introduction to migrations for more information.
The dotnet ef database update command runs the Up method in the Migrations/<time-stamp>_InitialCreate.cs
file, which creates the database.
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Create a couple more movie entries. Try the Edit, Details, and Delete links, which are all functional.
You now have a database and pages to display, edit, update and delete data. In the next tutorial, we'll work with the
database.
Additional resources
Tag Helpers
Globalization and localization
P R E V IO U S A D D IN G A N E X T W O R K IN G W IT H
V IE W SQL
Work with SQLite in an ASP.NET Core MVC app
6/26/2018 • 2 minutes to read • Edit Online
By Rick Anderson
The MvcMovieContext object handles the task of connecting to the database and mapping Movie objects to
database records. The database context is registered with the Dependency Injection container in the
ConfigureServices method in the Startup.cs file:
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));
SQLite
The SQLite website states:
There are many third party tools you can download to manage and view a SQLite database. The image below is
from DB Browser for SQLite. If you have a favorite SQLite tool, leave a comment on what you like about it.
Seed the database
Create a new class named SeedData in the Models folder. Replace the generated code with the following:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the DB, the seed initializer returns.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
using MvcMovie;
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
try
{
// Requires using MvcMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal. We don't want to see the time (12:00:00
AM in the following image) and ReleaseDate should be two words.
Open the Models/Movie.cs file and add the highlighted lines shown below:
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
The Edit, Details, and Delete links are generated by the Core MVC Anchor Tag Helper in the
Views/Movies/Index.cshtml file.
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
code above, the AnchorTagHelper dynamically generates the HTML href attribute value from the controller action
method and route id. You use View Source from your favorite browser or use the developer tools to examine the
generated markup. A portion of the generated HTML is shown below:
<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>
ASP.NET Core translates http://localhost:1234/Movies/Edit/4 into a request to the Edit action method of the
Movies controller with the parameter Id of 4. ( Controller methods are also known as action methods.)
Tag Helpers are one of the most popular new features in ASP.NET Core. See Additional resources for more
information.
Open the Movies controller and examine the two Edit action methods. The following code shows the
HTTP GET Edit method, which fetches the movie and populates the edit form generated by the Edit.cshtml Razor
file.
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [Bind] attribute is one way to protect against over-posting. You should only include properties in the [Bind]
attribute that you want to change. See Protect your controller from over-posting for more information.
ViewModels provide an alternative approach to prevent over-posting.
Notice the second Edit action method is preceded by the [HttpPost] attribute.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The HttpPost attribute specifies that this Edit method can be invoked only for POST requests. You could apply
the [HttpGet] attribute to the first edit method, but that's not necessary because [HttpGet] is the default.
The ValidateAntiForgeryToken attribute is used to prevent forgery of a request and is paired up with an anti-
forgery token generated in the edit view file (Views/Movies/Edit.cshtml). The edit view file generates the anti-
forgery token with the Form Tag Helper.
<form asp-action="Edit">
The Form Tag Helper generates a hidden anti-forgery token that must match the [ValidateAntiForgeryToken]
generated anti-forgery token in the Edit method of the Movies controller. For more information, see Anti-
Request Forgery.
The method takes the movie ID parameter, looks up the movie using the Entity Framework
HttpGet Edit
SingleOrDefaultAsync method, and returns the selected movie to the Edit view. If a movie cannot be found,
NotFound ( HTTP 404 ) is returned.
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
When the scaffolding system created the Edit view, it examined the Movie class and created code to render
<label> and <input> elements for each property of the class. The following example shows the Edit view that
was generated by the Visual Studio scaffolding system:
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<form asp-action="Edit">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Genre" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
<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.
The <input> elements are in an HTML <form> element whose action attribute is set to post to the
/Movies/Edit/id URL. The form data will be posted to the server when the Save button is clicked. The last line
before the closing </form> element shows the hidden XSRF token generated by the Form Tag Helper.
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [ValidateAntiForgeryToken] attribute validates the hidden XSRF token generated by the anti-forgery token
generator in the Form Tag Helper
The model binding system takes the posted form values and creates a Movie object that's passed as the movie
parameter. The ModelState.IsValid method verifies that the data submitted in the form can be used to modify
(edit or update) a Movie object. If the data is valid it's saved. The updated (edited) movie data is saved to the
database by calling the SaveChangesAsync method of database context. After saving the data, the code redirects the
user to the Index action method of the MoviesController class, which displays the movie collection, including the
changes just made.
Before the form is posted to the server, client side validation checks any validation rules on the fields. If there are
any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't
have client side validation but the server will detect the posted values that are not valid, and the form values will
be redisplayed with error messages. Later in the tutorial we examine Model Validation in more detail. The
Validation Tag Helper in the Views/Movies/Edit.cshtml view template takes care of displaying appropriate error
messages.
All the HttpGet methods in the movie controller follow a similar pattern. They get a movie object (or list of
objects, in the case of Index ), and pass the object (model) to the view. The Create method passes an empty
movie object to the Create view. All the methods that create, edit, delete, or otherwise modify data do so in the
[HttpPost] overload of the method. Modifying data in an HTTP GET method is a security risk. Modifying data in
an HTTP GET method also violates HTTP best practices and the architectural REST pattern, which specifies that
GET requests shouldn't change the state of your application. In other words, performing a GET operation should
be a safe operation that has no side effects and doesn't modify your persisted data.
Additional resources
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
Anti-Request Forgery
Protect your controller from over-posting
ViewModels
Form Tag Helper
Input Tag Helper
Label Tag Helper
Select Tag Helper
Validation Tag Helper
P R E V IO U S - W O R K IN G W IT H NE X T - ADD
S Q L IT E SE A RCH
Add search to an ASP.NET Core MVC app
6/26/2018 • 7 minutes to read • Edit Online
By Rick Anderson
In this section you add search capability to the Index action method that lets you search movies by genre or
name.
Update the Index method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the Index action method creates a LINQ query to select the movies:
The query is only defined at this point, it has not been run against the database.
If the searchString parameter contains a string, the movies query is modified to filter on the value of the search
string:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code above is a Lambda Expression. Lambdas are used in method-based LINQ
queries as arguments to standard query operator methods such as the Where method or Contains (used in the
code above). LINQ queries are not executed when they're defined or when they're modified by calling a method
such as Where , Contains or OrderBy . Rather, query execution is deferred. That means that the evaluation of an
expression is delayed until its realized value is actually iterated over or the ToListAsync method is called. For more
information about deferred query execution, see Query Execution.
Note: The Contains method is run on the database, not in the c# code shown above. The case sensitivity on the
query depends on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case
insensitive. In SQLlite, with the default collation, it's case sensitive.
Navigate to /Movies/Index . Append a query string such as ?searchString=Ghost to the URL. The filtered movies
are displayed.
If you change the signature of the Index method to have a parameter named id , the id parameter will match
the optional {id} placeholder for the default routes set in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Note: SQLlite is case sensitive, so you'll need to search for "Ghost" and not "ghost".
The previous Index method:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}
However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add
UI elements to help them filter movies. If you changed the signature of the Index method to test how to pass the
route-bound ID parameter, change it back so that it takes a parameter named searchString :
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
Open the Views/Movies/Index.cshtml file, and add the <form> markup highlighted below:
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
The HTML <form> tag uses the Form Tag Helper, so when you submit the form, the filter string is posted to the
Index action of the movies controller. Save your changes and then test the filter.
There's no [HttpPost] overload of the Index method as you might expect. You don't need it, because the method
isn't changing the state of the app, just filtering data.
You could add the following [HttpPost] Index method.
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
The notUsed parameter is used to create an overload for the Index method. We'll talk about that later in the
tutorial.
If you add this method, the action invoker would match the [HttpPost] Index method, and the [HttpPost] Index
method would run as shown in the image below.
However, even if you add this [HttpPost] version of the Index method, there's a limitation in how this has all
been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends
that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request
is the same as the URL for the GET request (localhost:xxxxx/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. We'll fix this by specifying the request should be HTTP GET .
Change the <form> tag in the Views\movie\Index.cshtml Razor view to specify method="get" :
<form asp-controller="Movies" asp-action="Index" method="get">
Now when you submit a search, the URL contains the search query string. Searching will also go to the
HttpGet Index action method, even if you have a HttpPost Index method.
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}
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.
The SelectList of genres is created by projecting the distinct genres (we don't want our select list to have
duplicate genres).
@{
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.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.movies[0].Title)
In the preceding code, the DisplayNameFor HTML Helper inspects the Title property referenced in the lambda
expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you
don't receive an access violation when model , model.movies , or model.movies[0] are null or empty. When the
lambda expression is evaluated (for example, @Html.DisplayFor(modelItem => item.Title) ), the model's property
values are evaluated.
Test the app by searching by genre, by movie title, and by both.
P R E V IO U S - C O N T R O L L E R M E T H O D S A N D NE X T - ADD A
V IE W S F IE L D
Add a new field to an ASP.NET Core MVC app
6/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
This tutorial will add a new field to the Movies table. We'll drop the database and create a new one when we
change the schema (add a new field). This workflow works well early in development when we don't have any
production data to perserve.
Once your app is deployed and you have data that you need to perserve, you can't drop your DB when you need
to change the schema. Entity Framework Code First Migrations allows you to update your schema and migrate
the database without losing data. Migrations is a popular feature when using SQL Server, but SQLlite doesn't
support many migration schema operations, so only very simply migrations are possible. See SQLite Limitations
for more information.
Because you've added a new field to the Movie class, you also need to update the binding whitelist 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:
[Bind("ID,Title,ReleaseDate,Genre,Price,Rating")]
You also need to update the view templates in order to display, create, and edit the new Rating property in the
browser view.
Edit the /Views/Movies/Index.cshtml file and add a Rating field:
<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>
You're seeing this error because the updated Movie model class is different than the schema of the Movie table of
the existing database. (There's no Rating column in the database table.)
There are a few approaches to resolving the error:
1. Drop the database and have the Entity Framework automatically re-create the database based on the new
model class schema. With this approach, you lose existing data in the database — so you can't do this with a
production database! Using an initializer to automatically seed a database with test data is often a
productive way to develop an app.
2. Manually modify the schema of the existing database so that it matches the model classes. The advantage
of this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, we'll drop and re-create the database when the schema changes. Run the following command
from a terminal to drop the db:
dotnet ef database drop
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 .
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},
Add the Rating field to the Edit , Details , and Delete view.
Run the app and verify you can create/edit/display movies with a Rating field. templates.
P R E V IO U S - A D D NE X T - ADD
SE A RCH V A L ID A T IO N
Add validation to an ASP.NET Core MVC app
6/26/2018 • 10 minutes to read • Edit Online
By Rick Anderson
In this section you'll add validation logic to the Movie model, and you'll ensure that the validation rules are
enforced any time a user creates or edits a movie.
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
public class Movie
{
public int ID { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
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 indicates that a property must have a value; but nothing prevents a user
from entering white space to satisfy this validation. The RegularExpression attribute is used to limit what
characters can be input. In the code above, Genre and Rating must use only letters (First letter uppercase, white
space, numbers and special characters are not allowed). The Range attribute constrains a value to within a
specified range. The StringLength attribute lets you set the maximum length of a string property, and optionally
its minimum length. Value types (such as decimal , int , float , DateTime ) are inherently required and don't need
the [Required] attribute.
Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also
ensures that you can't forget to validate something and inadvertently let bad data into the database.
Notice how the form has automatically rendered an appropriate validation error message in each field containing
an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a
user has JavaScript disabled).
A significant benefit is that you didn't need to change a single line of code in the MoviesController class or in the
Create.cshtml view in order to enable this validation UI. The controller and views you created earlier in this tutorial
automatically picked up the validation rules that you specified by using validation attributes on the properties of
the Movie model class. Test validation using the Edit action method, and the same validation is applied.
The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a
break point in the HTTP Post method, by using the Fiddler tool , or the F12 Developer tools.
// GET: Movies/Create
public IActionResult Create()
{
return View();
}
// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}
The first (HTTP GET) Create action method displays the initial Create form. The second ( [HttpPost] ) version
handles the form post. The second Create method (The [HttpPost] version) calls ModelState.IsValid to check
whether the movie has any validation errors. Calling this method evaluates any validation attributes that have
been applied to the object. If the object has validation errors, the Create method re-displays the form. If there are
no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the
server when there are validation errors detected on the client side; the second Create method is never called
when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled
and you can test the HTTP POST Create method ModelState.IsValid detecting any validation errors.
You can set a break point in the [HttpPost] Create method and verify the method is never called, client side
validation won't submit the form data when validation errors are detected. If you disable JavaScript in your
browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.
The following image shows how to disable JavaScript in the FireFox browser.
The following image shows how to disable JavaScript in the Chrome browser.
After you disable JavaScript, post invalid data and step through the debugger.
Below is portion of the Create.cshtml view template that you scaffolded earlier in the tutorial. It's used by the
action methods shown above both to display the initial form and to redisplay it in the event of an error.
<form asp-action="Create">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery
Validation on the client side. The Validation Tag Helper displays validation errors. See Validation for more
information.
What's really nice about this approach is that neither the controller nor the Create view template knows anything
about the actual validation rules being enforced or about the specific error messages displayed. The validation
rules and the error strings are specified only in the Movie class. These same validation rules are automatically
applied to the Edit view and any other views templates you might create that edit your model.
When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the
model (in this example, the Movie class). You won't have to worry about different parts of the application being
inconsistent with how the rules are enforced — all validation logic will be defined in one place and used
everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be
fully honoring the DRY principle.
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
The DataType attributes only provide hints for the view engine to format the data (and supplies
elements/attributes such as <a> for URL's and <a href="mailto:EmailAddress.com"> for email. You can use the
RegularExpression attribute to validate the format of the data. The DataType attribute is used to specify a data
type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want
to keep track of the date, not the time. The DataType Enumeration provides for many data types, such as Date,
Time, PhoneNumber, Currency, EmailAddress and more. The DataType attribute can also enable the application to
automatically provide type-specific features. For example, a mailto: link can be created for
DataType.EmailAddress , and a date selector can be provided for DataType.Date in browsers that support HTML5.
The DataType attributes emit HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers can
understand. The DataType attributes do not provide any validation.
DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed
according to the default formats based on the server's CultureInfo .
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should also be applied when the value is displayed
in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably
don't want the currency symbol in the text box for editing.)
You can use the DisplayFormat attribute by itself, but it's generally a good idea to use the DataType attribute. The
DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the
following benefits that you don't get with DisplayFormat:
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, etc.)
By default, the browser will render data using the correct format based on your locale.
The DataType attribute can enable MVC to choose the right field template to render the data (the
DisplayFormat if used by itself uses the string template).
NOTE
jQuery validation doesn't work with the Range attribute and DateTime . For example, the following code will always display
a client side validation error, even when the date is in the specified range:
You will need to disable jQuery date validation to use the Range attribute with DateTime . It's generally not a good
practice to compile hard dates in your models, so using the Range attribute and DateTime is discouraged.
The following code shows combining attributes on one line:
public class Movie
{
public int ID { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
In the next part of the series, we'll review the application and make some improvements to the automatically
generated Details and Delete methods.
Additional resources
Working with Forms
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
P R E V IO U S - A D D A N E X T - E X A M IN E T H E D E T A IL S A N D D E L E T E
F IE L D M E THOD S
Examine the Details and Delete methods of an
ASP.NET Core app
9/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
Open the Movie controller and examine the Details method:
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that
invokes the method. In this case it's a GET request with three URL segments, the Movies controller, the Details
method and an id value. Recall these segments are defined in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
EF makes it easy to search for data using the SingleOrDefaultAsync method. An important security feature built
into the method is that the code verifies that the search method has found a movie before it tries to do anything
with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from
http://localhost:xxxx/Movies/Details/1 to something like http://localhost:xxxx/Movies/Details/12345 (or some
other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an
exception.
Examine the Delete and DeleteConfirmed methods.
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
Note that the HTTP GET Delete method doesn't delete the specified movie, it returns a view of the movie where you
can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter,
performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.
The [HttpPost] method that deletes the data is named DeleteConfirmed to give the HTTP POST method a unique
signature or name. The two method signatures are shown below:
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
The common language runtime (CLR ) requires overloaded methods to have a unique parameter signature (same
method name but different list of parameters). However, here you need two Delete methods -- one for GET and
one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a
parameter.)
There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding
mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a
URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that
method. The solution is what you see in the example, which is to add the ActionName("Delete") attribute to the
DeleteConfirmed method. That attribute performs mapping for the routing system so that a URL that includes
/Delete/ for a POST request will find the DeleteConfirmed method.
Another common work around for methods that have identical names and signatures is to artificially change the
signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when
we added the notUsed parameter. You could do the same thing here for the [HttpPost] Delete method:
// POST: Movies/Delete/6
[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 app in Azure with SQL Database. The
instruction are for an ASP.NET app, not an ASP.NET Core app, but the steps are the same.
P R E V IO U S
Create an ASP.NET Core MVC app with Visual Studio
Code
6/21/2018 • 2 minutes to read • Edit Online
This series of tutorials teaches you the basics of building an ASP.NET Core MVC web app using Visual Studio
Code.
This tutorial teaches ASP.NET Core MVC web development with controllers and views. Razor Pages is a feature of
the ASP.NET Core MVC framework that makes building and testing web UI easier and more productive. You can
use Razor pages alongside controllers and views in the same project.
We recommend you try the Razor Pages tutorial before the MVC/Controller/Views version. The Razor Pages
tutorial:
Is the preferred approach for new application development.
Is easier to follow.
Covers more features.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
1. Get started
2. Add a controller
3. Add a view
4. Add a model
5. Work with SQLite
6. Controller methods and views
7. Add search
8. Add a new field
9. Add validation
10. Examine the Details and Delete methods
Introduction to ASP.NET Core MVC on macOS,
Linux, or Windows
6/21/2018 • 2 minutes to read • Edit Online
By Rick Anderson
This tutorial will teach you the basics of building an ASP.NET Core MVC web app using Visual Studio Code (VS
Code). The tutorial assumes familarity with VS Code. See Getting started with VS Code and Visual Studio Code
help for more information.
This tutorial teaches ASP.NET Core MVC web development with controllers and views. Razor Pages is a feature
of the ASP.NET Core MVC framework that makes building and testing web UI easier and more productive. You
can use Razor pages alongside controllers and views in the same project.
We recommend you try the Razor Pages tutorial before the MVC/Controller/Views version. The Razor Pages
tutorial:
Is the preferred approach for new application development.
Is easier to follow.
Covers more features.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
There are 3 versions of this tutorial:
macOS: Create an ASP.NET Core MVC app with Visual Studio for Mac
Windows: Create an ASP.NET Core MVC app with Visual Studio
macOS, Linux, and Windows: Create an ASP.NET Core MVC app with Visual Studio Code
Prerequisites
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code
mkdir MvcMovie
cd MvcMovie
dotnet new mvc
Open the MvcMovie folder in Visual Studio Code (VS Code) and select the Startup.cs file.
Select Yes to the Warn message "Required assets to build and debug are missing from 'MvcMovie'. Add
them?"
Select Restore to the Info message "There are unresolved dependencies".
VS Code starts the Kestrel web server and runs your app. Notice that the address bar shows localhost:5000 and
not something like example.com . That's because localhost is the standard hostname for your local computer.
The default template gives you working Home, About and Contact links. The browser image above doesn't
show these links. Depending on the size of your browser, you might need to click the navigation icon to show
them.
In the next part of this tutorial, we'll learn about MVC and start writing some code.
NE X T - ADD A
C ON TROL L E R
Add a controller to an ASP.NET Core app
6/21/2018 • 5 minutes to read • Edit Online
By Rick Anderson
The Model-View -Controller (MVC ) architectural pattern separates an app into three main components: Model,
View, and Controller. The MVC pattern helps you create apps that are more testable and easier to update than
traditional monolithic apps. MVC -based apps contain:
Models: Classes that represent the data of the app. The model classes use validation logic to enforce
business rules for that data. Typically, model objects retrieve and store model state in a database. In this
tutorial, a Movie model retrieves movie data from a database, provides it to the view or updates it. Updated
data is written to a database.
Views: Views are the components that display the app's user interface (UI). Generally, this UI displays the
model data.
Controllers: Classes that handle browser requests. They retrieve model data and call view templates that
return a response. In an MVC app, the view only displays information; the controller handles and responds
to user input and interaction. For example, the controller handles route data and query-string values, and
passes these values to the model. The model might use these values to query the database. For example,
http://localhost:1234/Home/About has route data of Home (the controller ) and About (the action method to
call on the home controller). http://localhost:1234/Movies/Edit/5 is a request to edit the movie with ID=5
using the movie controller. We'll talk about route data later in the tutorial.
The MVC pattern helps you create apps that separate the different aspects of the app (input logic, business logic,
and UI logic), while providing a loose coupling between these elements. The pattern specifies where each kind of
logic should be located in the app. The UI logic belongs in the view. Input logic belongs in the controller. Business
logic belongs in the model. This separation helps you manage complexity when you build an app, because it
enables you to work on one aspect of the implementation at a time without impacting the code of another. For
example, you can work on the view code without depending on the business logic code.
We cover these concepts in this tutorial series and show you how to use them to build a movie app. The MVC
project contains folders for the Controllers and Views.
In VS Code, select the EXPLORER icon and then control-click (right-click) Controllers > New File and
name the new file HelloWorldController.cs.
Replace the contents of Controllers/HelloWorldController.cs with the following:
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
//
// GET: /HelloWorld/Welcome/
Every public method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a
string. Note the comments preceding each method.
An HTTP endpoint is a targetable URL in the web application, such as http://localhost:1234/HelloWorld , and
combines the protocol used: HTTP , the network location of the web server (including the TCP port):
localhost:1234 and the target URI HelloWorld .
The first comment states this is an HTTP GET method that's invoked by appending "/HelloWorld/" to the base
URL. The second comment specifies an HTTP GET method that's invoked by appending "/HelloWorld/Welcome/"
to the URL. Later on in the tutorial you'll use the scaffolding engine to generate HTTP POST methods.
Run the app in non-debug mode and append "HelloWorld" to the path in the address bar. The Index method
returns a string.
MVC invokes controller classes (and the action methods within them) depending on the incoming URL. The
default URL routing logic used by MVC uses a format like this to determine what code to invoke:
/[Controller]/[ActionName]/[Parameters]
You set the format for routing in the Configure method in Startup.cs file.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
When you run the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index"
method specified in the template line highlighted above.
The first URL segment determines the controller class to run. So localhost:xxxx/HelloWorld maps to the
HelloWorldController class. The second part of the URL segment determines the action method on the class. So
localhost:xxxx/HelloWorld/Index would cause the Index method of the HelloWorldController class to run. Notice
that you only had to browse to localhost:xxxx/HelloWorld and the Index method was called by default. This is
because Index is the default method that will be called on a controller if a method name isn't explicitly specified.
The third part of the URL segment ( id ) is for route data. You'll see route data later on in this tutorial.
Browse to http://localhost:xxxx/HelloWorld/Welcome . The Welcome method runs and returns the string "This is the
Welcome action method...". For this URL, the controller is HelloWorld and Welcome is the action method. You
haven't used the [Parameters] part of the URL yet.
Modify the code to pass some parameter information from the URL to the controller. For example,
/HelloWorld/Welcome?name=Rick&numtimes=4 . Change the Welcome method to include two parameters as shown in
the following code.
// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
(Replace xxxx with your port number.) You can try different values for name and numtimes in the URL. The MVC
model binding system automatically maps the named parameters from the query string in the address bar to
parameters in your method. See Model Binding for more information.
In the image above, the URL segment ( Parameters ) isn't used, the name and numTimes parameters are passed as
query strings. The ? (question mark) in the above URL is a separator, and the query strings follow. The &
character separates query strings.
Replace the Welcome method with the following code:
This time the third URL segment matched the route parameter id . The Welcome method contains a parameter
id that matched the URL template in the MapRoute method. The trailing ? (in id? ) indicates the id parameter
is optional.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
In these examples the controller has been doing the "VC" portion of MVC - that is, the view and controller work.
The controller is returning HTML directly. Generally you don't want controllers returning HTML directly, since that
becomes very cumbersome to code and maintain. Instead you typically use a separate Razor view template file to
help generate the HTML response. You do that in the next tutorial.
P R E V IO U S - A D D A NE X T - ADD A
C ON TROL L E R V IE W
Add a view to an ASP.NET Core MVC app
6/26/2018 • 8 minutes to read • Edit Online
By Rick Anderson
In this section you modify the HelloWorldController class to use Razor view template files to cleanly encapsulate
the process of generating HTML responses to a client.
You create a view template file using Razor. Razor-based view templates have a .cshtml file extension. They
provide an elegant way to create HTML output using C#.
Currently the method returns a string with a message that's hard-coded in the controller class. In the
Index
HelloWorldController class, replace the Index method with the following code:
The preceding code returns a View object. It uses a view template to generate an HTML response to the browser.
Controller methods (also known as action methods) such as the Index method above, generally return an
IActionResult (or a class derived from ActionResult ), not a type like string.
Add an Index view for the HelloWorldController .
Add a new folder named Views/HelloWorld.
Add a new file to the Views/HelloWorld folder name Index.cshtml.
Replace the contents of the Views/HelloWorld/Index.cshtml Razor view file with the following:
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Movies" asp-action="Index" class="navbar-brand">Movie App</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - MvcMovie</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Movies" asp-action="Index" class="navbar-brand">Movie App</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2017 - MvcMovie</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
WARNING
We haven't implemented the Movies controller yet, so if you click on that link, you'll get a 404 (Not found) error.
Save your changes and tap the About link. Notice how the title on the browser tab now displays About - Movie
App instead of About - Mvc Movie:
Tap the Contact link and notice that the title and anchor text also display Movie App. We were able to make the
change once in the layout template and have all pages on the site reflect the new link text and new title.
Examine the Views/_ViewStart.cshtml file:
@{
Layout = "_Layout";
}
The Views/_ViewStart.cshtml file brings in the Views/Shared/_Layout.cshtml file to each view. You can use the
Layout property to set a different layout view, or set it to null so no layout file will be used.
You'll make them slightly different so you can see which bit of code changes which part of the app.
@{
ViewData["Title"] = "Movie List";
}
ViewData["Title"] = "Movie List"; in the code above sets the Title property of the ViewData dictionary to
"Movie List". The Title property is used in the <title> HTML element in the layout page:
Save your change and navigate to http://localhost:xxxx/HelloWorld . Notice that the browser title, the primary
heading, and the secondary headings have changed. (If you don't see changes in the browser, you might be
viewing cached content. Press Ctrl+F5 in your browser to force the response from the server to be loaded.) The
browser title is created with ViewData["Title"] we set in the Index.cshtml view template and the additional "-
Movie App" added in the layout file.
Also notice how the content in the Index.cshtml view template was merged with the Views/Shared/_Layout.cshtml
view template and a single HTML response was sent to the browser. Layout templates make it really easy to make
changes that apply across all of the pages in your application. To learn more see Layout.
Our little bit of "data" (in this case the "Hello from our View Template!" message) is hard-coded, though. The MVC
application has a "V" (view ) and you've got a "C" (controller), but no "M" (model) yet.
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}
return View();
}
}
}
The ViewData dictionary object contains data that will be passed to the view.
Create a Welcome view template named Views/HelloWorld/Welcome.cshtml.
You'll create a loop in the Welcome.cshtml view template that displays "Hello" NumTimes . Replace the contents of
Views/HelloWorld/Welcome.cshtml with the following:
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
Data is taken from the URL and passed to the controller using the MVC model binder . The controller packages
the data into a ViewData dictionary and passes that object to the view. The view then renders the data as HTML to
the browser.
In the sample above, we used the ViewData dictionary to pass data from the controller to a view. Later in the
tutorial, we will use a view model to pass data from a controller to a view. The view model approach to passing
data is generally much preferred over the ViewData dictionary approach. See ViewModel vs ViewData vs
ViewBag vs TempData vs Session in MVC for more information.
Well, that was a kind of an "M" for model, but not the database kind. Let's take what we've learned and create a
database of movies.
P R E V IO U S - A D D A NE X T - ADD A
C ON TROL L E R M ODEL
Add a model to an ASP.NET Core MVC app
6/26/2018 • 4 minutes to read • Edit Online
using System;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Save the file and select Restore to the Info message "There are unresolved dependencies".
Create a Models/MvcMovieContext.cs file and add the following MvcMovieContext class:
using Microsoft.EntityFrameworkCore;
namespace MvcMovie.Models
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
namespace MvcMovie
{
public class Startup
{
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));
This tells Entity Framework which model classes are included in the data model. You're defining one entity
set of Movie objects, which will be represented in the database as a Movie table.
Build the project to verify there are no errors.
Scaffold the MovieController
Open a terminal window in the project folder and run the following commands:
dotnet restore
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --
relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries
The dotnet ef migrations add InitialCreate command generates code to create the initial database schema. The
schema is based on the model specified in the DbContext (In the Models/MvcMovieContext.cs file). The Initial
argument is used to name the migrations. You can use any name, but by convention you choose a name that
describes the migration. See Introduction to migrations for more information.
The dotnet ef database update command runs the Up method in the Migrations/<time-stamp>_InitialCreate.cs
file, which creates the database.
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}
Create a couple more movie entries. Try the Edit, Details, and Delete links, which are all functional.
You now have a database and pages to display, edit, update and delete data. In the next tutorial, we'll work with the
database.
Additional resources
Tag Helpers
Globalization and localization
P R E V IO U S - A D D A N E X T - W O R K IN G W IT H
V IE W S Q L IT E
Work with SQLite in an ASP.NET Core MVC app
6/26/2018 • 2 minutes to read • Edit Online
By Rick Anderson
The MvcMovieContext object handles the task of connecting to the database and mapping Movie objects to
database records. The database context is registered with the Dependency Injection container in the
ConfigureServices method in the Startup.cs file:
services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlite("Data Source=MvcMovie.db"));
SQLite
The SQLite website states:
There are many third party tools you can download to manage and view a SQLite database. The image below is
from DB Browser for SQLite. If you have a favorite SQLite tool, leave a comment on what you like about it.
Seed the database
Create a new class named SeedData in the Models folder. Replace the generated code with the following:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}
If there are any movies in the DB, the seed initializer returns.
if (context.Movie.Any())
{
return; // DB has been seeded.
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;
using MvcMovie;
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<MvcMovieContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
try
{
// Requires using MvcMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
By Rick Anderson
We have a good start to the movie app, but the presentation isn't ideal. We don't want to see the time (12:00:00
AM in the image below ) and ReleaseDate should be two words.
Open the Models/Movie.cs file and add the highlighted lines shown below:
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
The Edit, Details, and Delete links are generated by the Core MVC Anchor Tag Helper in the
Views/Movies/Index.cshtml file.
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the
code above, the AnchorTagHelper dynamically generates the HTML href attribute value from the controller action
method and route id. You use View Source from your favorite browser or use the developer tools to examine the
generated markup. A portion of the generated HTML is shown below:
<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>
ASP.NET Core translates http://localhost:1234/Movies/Edit/4 into a request to the Edit action method of the
Movies controller with the parameter Id of 4. ( Controller methods are also known as action methods.)
Tag Helpers are one of the most popular new features in ASP.NET Core. See Additional resources for more
information.
Open the Movies controller and examine the two Edit action methods. The following code shows the
HTTP GET Edit method, which fetches the movie and populates the edit form generated by the Edit.cshtml Razor
file.
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [Bind] attribute is one way to protect against over-posting. You should only include properties in the [Bind]
attribute that you want to change. See Protect your controller from over-posting for more information.
ViewModels provide an alternative approach to prevent over-posting.
Notice the second Edit action method is preceded by the [HttpPost] attribute.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The HttpPost attribute specifies that this Edit method can be invoked only for POST requests. You could apply
the [HttpGet] attribute to the first edit method, but that's not necessary because [HttpGet] is the default.
The ValidateAntiForgeryToken attribute is used to prevent forgery of a request and is paired up with an anti-
forgery token generated in the edit view file (Views/Movies/Edit.cshtml). The edit view file generates the anti-
forgery token with the Form Tag Helper.
<form asp-action="Edit">
The Form Tag Helper generates a hidden anti-forgery token that must match the [ValidateAntiForgeryToken]
generated anti-forgery token in the Edit method of the Movies controller. For more information, see Anti-
Request Forgery.
The method takes the movie ID parameter, looks up the movie using the Entity Framework
HttpGet Edit
SingleOrDefaultAsync method, and returns the selected movie to the Edit view. If a movie cannot be found,
NotFound ( HTTP 404 ) is returned.
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
When the scaffolding system created the Edit view, it examined the Movie class and created code to render
<label> and <input> elements for each property of the class. The following example shows the Edit view that
was generated by the Visual Studio scaffolding system:
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<form asp-action="Edit">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ID" />
<div class="form-group">
<label asp-for="Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Genre" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
<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.
The <input> elements are in an HTML <form> element whose action attribute is set to post to the
/Movies/Edit/id URL. The form data will be posted to the server when the Save button is clicked. The last line
before the closing </form> element shows the hidden XSRF token generated by the Form Tag Helper.
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}
The [ValidateAntiForgeryToken] attribute validates the hidden XSRF token generated by the anti-forgery token
generator in the Form Tag Helper
The model binding system takes the posted form values and creates a Movie object that's passed as the movie
parameter. The ModelState.IsValid method verifies that the data submitted in the form can be used to modify
(edit or update) a Movie object. If the data is valid it's saved. The updated (edited) movie data is saved to the
database by calling the SaveChangesAsync method of database context. After saving the data, the code redirects the
user to the Index action method of the MoviesController class, which displays the movie collection, including the
changes just made.
Before the form is posted to the server, client side validation checks any validation rules on the fields. If there are
any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't
have client side validation but the server will detect the posted values that are not valid, and the form values will
be redisplayed with error messages. Later in the tutorial we examine Model Validation in more detail. The
Validation Tag Helper in the Views/Movies/Edit.cshtml view template takes care of displaying appropriate error
messages.
All the HttpGet methods in the movie controller follow a similar pattern. They get a movie object (or list of
objects, in the case of Index ), and pass the object (model) to the view. The Create method passes an empty
movie object to the Create view. All the methods that create, edit, delete, or otherwise modify data do so in the
[HttpPost] overload of the method. Modifying data in an HTTP GET method is a security risk. Modifying data in
an HTTP GET method also violates HTTP best practices and the architectural REST pattern, which specifies that
GET requests shouldn't change the state of your application. In other words, performing a GET operation should
be a safe operation that has no side effects and doesn't modify your persisted data.
Additional resources
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
Anti-Request Forgery
Protect your controller from over-posting
ViewModels
Form Tag Helper
Input Tag Helper
Label Tag Helper
Select Tag Helper
Validation Tag Helper
P R E V IO U S - W O R K IN G W IT H NE X T - ADD
S Q L IT E SE A RCH
Add search to an ASP.NET Core MVC app
6/26/2018 • 7 minutes to read • Edit Online
By Rick Anderson
In this section you add search capability to the Index action method that lets you search movies by genre or
name.
Update the Index method with the following code:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The first line of the Index action method creates a LINQ query to select the movies:
The query is only defined at this point, it has not been run against the database.
If the searchString parameter contains a string, the movies query is modified to filter on the value of the search
string:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
The s => s.Title.Contains() code above is a Lambda Expression. Lambdas are used in method-based LINQ
queries as arguments to standard query operator methods such as the Where method or Contains (used in the
code above). LINQ queries are not executed when they're defined or when they're modified by calling a method
such as Where , Contains or OrderBy . Rather, query execution is deferred. That means that the evaluation of an
expression is delayed until its realized value is actually iterated over or the ToListAsync method is called. For more
information about deferred query execution, see Query Execution.
Note: The Contains method is run on the database, not in the c# code shown above. The case sensitivity on the
query depends on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case
insensitive. In SQLlite, with the default collation, it's case sensitive.
Navigate to /Movies/Index . Append a query string such as ?searchString=Ghost to the URL. The filtered movies
are displayed.
If you change the signature of the Index method to have a parameter named id , the id parameter will match
the optional {id} placeholder for the default routes set in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Note: SQLlite is case sensitive, so you'll need to search for "Ghost" and not "ghost".
The previous Index method:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}
However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add
UI elements to help them filter movies. If you changed the signature of the Index method to test how to pass the
route-bound ID parameter, change it back so that it takes a parameter named searchString :
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
Open the Views/Movies/Index.cshtml file, and add the <form> markup highlighted below:
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
The HTML <form> tag uses the Form Tag Helper, so when you submit the form, the filter string is posted to the
Index action of the movies controller. Save your changes and then test the filter.
There's no [HttpPost] overload of the Index method as you might expect. You don't need it, because the method
isn't changing the state of the app, just filtering data.
You could add the following [HttpPost] Index method.
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
The notUsed parameter is used to create an overload for the Index method. We'll talk about that later in the
tutorial.
If you add this method, the action invoker would match the [HttpPost] Index method, and the [HttpPost] Index
method would run as shown in the image below.
However, even if you add this [HttpPost] version of the Index method, there's a limitation in how this has all
been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends
that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request
is the same as the URL for the GET request (localhost:xxxxx/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. We'll fix this by specifying the request should be HTTP GET .
Change the <form> tag in the Views\movie\Index.cshtml Razor view to specify method="get" :
<form asp-controller="Movies" asp-action="Index" method="get">
Now when you submit a search, the URL contains the search query string. Searching will also go to the
HttpGet Index action method, even if you have a HttpPost Index method.
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}
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.
The SelectList of genres is created by projecting the distinct genres (we don't want our select list to have
duplicate genres).
@{
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.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Examine the lambda expression used in the following HTML Helper:
@Html.DisplayNameFor(model => model.movies[0].Title)
In the preceding code, the DisplayNameFor HTML Helper inspects the Title property referenced in the lambda
expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you
don't receive an access violation when model , model.movies , or model.movies[0] are null or empty. When the
lambda expression is evaluated (for example, @Html.DisplayFor(modelItem => item.Title) ), the model's property
values are evaluated.
Test the app by searching by genre, by movie title, and by both.
P R E V IO U S - C O N T R O L L E R M E T H O D S A N D NE X T - ADD A
V IE W S F IE L D
Add a new field to an ASP.NET Core MVC app
6/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
This tutorial will add a new field to the Movies table. We'll drop the database and create a new one when we
change the schema (add a new field). This workflow works well early in development when we don't have any
production data to perserve.
Once your app is deployed and you have data that you need to perserve, you can't drop your DB when you need
to change the schema. Entity Framework Code First Migrations allows you to update your schema and migrate
the database without losing data. Migrations is a popular feature when using SQL Server, but SQLlite doesn't
support many migration schema operations, so only very simply migrations are possible. See SQLite Limitations
for more information.
Because you've added a new field to the Movie class, you also need to update the binding whitelist 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:
[Bind("ID,Title,ReleaseDate,Genre,Price,Rating")]
You also need to update the view templates in order to display, create, and edit the new Rating property in the
browser view.
Edit the /Views/Movies/Index.cshtml file and add a Rating field:
<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>
You're seeing this error because the updated Movie model class is different than the schema of the Movie table of
the existing database. (There's no Rating column in the database table.)
There are a few approaches to resolving the error:
1. Drop the database and have the Entity Framework automatically re-create the database based on the new
model class schema. With this approach, you lose existing data in the database — so you can't do this with a
production database! Using an initializer to automatically seed a database with test data is often a
productive way to develop an app.
2. Manually modify the schema of the existing database so that it matches the model classes. The advantage
of this approach is that you keep your data. You can make this change either manually or by creating a
database change script.
3. Use Code First Migrations to update the database schema.
For this tutorial, we'll drop and re-create the database when the schema changes. Run the following command
from a terminal to drop the db:
dotnet ef database drop
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 .
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},
Add the Rating field to the Edit , Details , and Delete view.
Run the app and verify you can create/edit/display movies with a Rating field. templates.
P R E V IO U S - A D D NE X T - ADD
SE A RCH V A L ID A T IO N
Add validation to an ASP.NET Core MVC app
6/26/2018 • 10 minutes to read • Edit Online
By Rick Anderson
In this section you'll add validation logic to the Movie model, and you'll ensure that the validation rules are
enforced any time a user creates or edits a movie.
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}
public class Movie
{
public int ID { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
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 indicates that a property must have a value; but nothing prevents a user
from entering white space to satisfy this validation. The RegularExpression attribute is used to limit what
characters can be input. In the code above, Genre and Rating must use only letters (First letter uppercase, white
space, numbers and special characters are not allowed). The Range attribute constrains a value to within a
specified range. The StringLength attribute lets you set the maximum length of a string property, and optionally
its minimum length. Value types (such as decimal , int , float , DateTime ) are inherently required and don't need
the [Required] attribute.
Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also
ensures that you can't forget to validate something and inadvertently let bad data into the database.
Notice how the form has automatically rendered an appropriate validation error message in each field containing
an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a
user has JavaScript disabled).
A significant benefit is that you didn't need to change a single line of code in the MoviesController class or in the
Create.cshtml view in order to enable this validation UI. The controller and views you created earlier in this tutorial
automatically picked up the validation rules that you specified by using validation attributes on the properties of
the Movie model class. Test validation using the Edit action method, and the same validation is applied.
The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a
break point in the HTTP Post method, by using the Fiddler tool , or the F12 Developer tools.
// GET: Movies/Create
public IActionResult Create()
{
return View();
}
// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(movie);
}
The first (HTTP GET) Create action method displays the initial Create form. The second ( [HttpPost] ) version
handles the form post. The second Create method (The [HttpPost] version) calls ModelState.IsValid to check
whether the movie has any validation errors. Calling this method evaluates any validation attributes that have
been applied to the object. If the object has validation errors, the Create method re-displays the form. If there are
no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the
server when there are validation errors detected on the client side; the second Create method is never called
when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled
and you can test the HTTP POST Create method ModelState.IsValid detecting any validation errors.
You can set a break point in the [HttpPost] Create method and verify the method is never called, client side
validation won't submit the form data when validation errors are detected. If you disable JavaScript in your
browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.
The following image shows how to disable JavaScript in the FireFox browser.
The following image shows how to disable JavaScript in the Chrome browser.
After you disable JavaScript, post invalid data and step through the debugger.
Below is portion of the Create.cshtml view template that you scaffolded earlier in the tutorial. It's used by the
action methods shown above both to display the initial form and to redisplay it in the event of an error.
<form asp-action="Create">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery
Validation on the client side. The Validation Tag Helper displays validation errors. See Validation for more
information.
What's really nice about this approach is that neither the controller nor the Create view template knows anything
about the actual validation rules being enforced or about the specific error messages displayed. The validation
rules and the error strings are specified only in the Movie class. These same validation rules are automatically
applied to the Edit view and any other views templates you might create that edit your model.
When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the
model (in this example, the Movie class). You won't have to worry about different parts of the application being
inconsistent with how the rules are enforced — all validation logic will be defined in one place and used
everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be
fully honoring the DRY principle.
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
The DataType attributes only provide hints for the view engine to format the data (and supplies
elements/attributes such as <a> for URL's and <a href="mailto:EmailAddress.com"> for email. You can use the
RegularExpression attribute to validate the format of the data. The DataType attribute is used to specify a data
type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want
to keep track of the date, not the time. The DataType Enumeration provides for many data types, such as Date,
Time, PhoneNumber, Currency, EmailAddress and more. The DataType attribute can also enable the application to
automatically provide type-specific features. For example, a mailto: link can be created for
DataType.EmailAddress , and a date selector can be provided for DataType.Date in browsers that support HTML5.
The DataType attributes emit HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers can
understand. The DataType attributes do not provide any validation.
DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed
according to the default formats based on the server's CultureInfo .
The DisplayFormat attribute is used to explicitly specify the date format:
The ApplyFormatInEditMode setting specifies that the formatting should also be applied when the value is displayed
in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably
don't want the currency symbol in the text box for editing.)
You can use the DisplayFormat attribute by itself, but it's generally a good idea to use the DataType attribute. The
DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the
following benefits that you don't get with DisplayFormat:
The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate
currency symbol, email links, etc.)
By default, the browser will render data using the correct format based on your locale.
The DataType attribute can enable MVC to choose the right field template to render the data (the
DisplayFormat if used by itself uses the string template).
NOTE
jQuery validation doesn't work with the Range attribute and DateTime . For example, the following code will always display
a client side validation error, even when the date is in the specified range:
You will need to disable jQuery date validation to use the Range attribute with DateTime . It's generally not a good
practice to compile hard dates in your models, so using the Range attribute and DateTime is discouraged.
The following code shows combining attributes on one line:
public class Movie
{
public int ID { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
In the next part of the series, we'll review the application and make some improvements to the automatically
generated Details and Delete methods.
Additional resources
Working with Forms
Globalization and localization
Introduction to Tag Helpers
Author Tag Helpers
P R E V IO U S - A D D A N E X T - E X A M IN E T H E D E T A IL S A N D D E L E T E
F IE L D M E THOD S
Examine the Details and Delete methods of an
ASP.NET Core app
9/26/2018 • 3 minutes to read • Edit Online
By Rick Anderson
Open the Movie controller and examine the Details method:
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that
invokes the method. In this case it's a GET request with three URL segments, the Movies controller, the Details
method and an id value. Recall these segments are defined in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
EF makes it easy to search for data using the SingleOrDefaultAsync method. An important security feature built
into the method is that the code verifies that the search method has found a movie before it tries to do anything
with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from
http://localhost:xxxx/Movies/Details/1 to something like http://localhost:xxxx/Movies/Details/12345 (or some
other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an
exception.
Examine the Delete and DeleteConfirmed methods.
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.SingleOrDefaultAsync(m => m.ID == id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
Note that the HTTP GET Delete method doesn't delete the specified movie, it returns a view of the movie where
you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that
matter, performing an edit operation, create operation, or any other operation that changes data) opens up a
security hole.
The [HttpPost] method that deletes the data is named DeleteConfirmed to give the HTTP POST method a
unique signature or name. The two method signatures are shown below:
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
The common language runtime (CLR ) requires overloaded methods to have a unique parameter signature (same
method name but different list of parameters). However, here you need two Delete methods -- one for GET and
one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a
parameter.)
There are two approaches to this problem, one is to give the methods different names. That's what the
scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps
segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able
to find that method. The solution is what you see in the example, which is to add the ActionName("Delete")
attribute to the DeleteConfirmed method. That attribute performs mapping for the routing system so that a URL
that includes /Delete/ for a POST request will find the DeleteConfirmed method.
Another common work around for methods that have identical names and signatures is to artificially change the
signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post
when we added the notUsed parameter. You could do the same thing here for the [HttpPost] Delete method:
// POST: Movies/Delete/6
[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 app in Azure with SQL Database. The
instruction are for an ASP.NET app, not an ASP.NET Core app, but the steps are the same.
P R E V IO U S
Create a Web API with ASP.NET Core and Visual
Studio for Mac
9/18/2018 • 16 minutes to read • Edit Online
Overview
This tutorial creates the following API:
GET /api/todo Get all to-do items None Array of to-do items
The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a
client. Postman or curl is used as the client to test the app.
A model is an object that represents the data in the app. In this case, the only model is a to-do item.
Models are represented as C# classes, also known as Plain Old CLR Object (POCOs).
A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single
controller.
To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items in
an in-memory database.
See Introduction to ASP.NET Core MVC on macOS or Linux for an example that uses a persistent database.
Prerequisites
Visual Studio for Mac
Select .NET Core App > ASP.NET Core Web API > Next.
Enter TodoApi for the Project Name, and then click Create.
NOTE
You can put model classes anywhere in your project, but the Models folder is used by convention.
Right-click the Models folder, and select Add > New File > General > Empty Class. Name the class TodoItem,
and then click New.
Replace the generated code with:
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}
Add a controller
In Solution Explorer, in the Controllers folder, add the class TodoController .
Replace the generated code with the following:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
The preceding code defines an API controller class without methods. In the next sections, methods are added to
implement the API.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
In the next sections, methods are added to implement the API. The class is annotated with an [ApiController]
attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate
class with ApiControllerAttribute.
The controller's constructor uses Dependency Injection to inject the database context ( TodoContext ) into the
controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an
item to the in-memory database if one doesn't exist.
[HttpGet]
public List<TodoItem> GetAll()
{
return _context.TodoItems.ToList();
}
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each
method is constructed as follows:
Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which is the controller class name minus the
"Controller" suffix. For this sample, the controller class name is TodoController and the root name is "todo".
ASP.NET Core routing is case insensitive.
If the [HttpGet] attribute has a route template (such as [HttpGet("/products")] , append that to the path. This
sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.
["value1","value2"]
[{"key":1,"name":"Item1","isComplete":false}]
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding method responds to an HTTP POST, as indicated by the [HttpPost] attribute. MVC gets the value
of the to-do item from the body of the HTTP request.
The CreatedAtRoute method returns a 201 response. It's the standard response for an HTTP POST method that
creates a new resource on the server. CreatedAtRoute also adds a Location header to the response. The Location
header specifies the URI of the newly created to-do item. See 10.2.2 201 Created.
Use Postman to send a Create request
Start the app (Run > Start With Debugging).
Open Postman.
{
"name":"walk dog",
"isComplete":true
}
TIP
If no response displays after clicking Send, disable the SSL certification verification option. This is found under File >
Settings. Click the Send button again after disabling the setting.
Click the Headers tab in the Response pane and copy the Location header value:
You can use the Location header URI to access the resource you created. The Create method returns
CreatedAtRoute. The first parameter passed to CreatedAtRoute represents the named route to use for generating
the URL. Recall that the GetById method created the "GetTodo" named route:
Update
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
Update is similar to Create , but uses HTTP PUT. The response is 204 (No Content). According to the HTTP
spec, a PUT request requires the client to send the entire updated entity, not just the deltas. To support partial
updates, use HTTP PATCH.
{
"key": 1,
"name": "walk dog",
"isComplete": true
}
Delete
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following
markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following
code:
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ 'display': 'block' });
}
$('.my-form').on('submit', function () {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open
launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to
open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a
complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the
experience richer. Below are explanations around the calls to the API.
Get a list of to -do items
To get a list of to-do items, send an HTTP GET request to /api/todo.
The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array.
This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url . GET is
used as the type . The success callback function is invoked if the request succeeds. In the callback, the DOM is
updated with the to-do information.
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
Next steps
For information on using a persistent database, see:
Create a Razor Pages web app with ASP.NET Core
Work with data in ASP.NET Core
ASP.NET Core Web API help pages using Swagger
Routing to controller actions
Build web APIs with ASP.NET Core
Controller action return types
For information about deploying an API, including to Azure App Service, see Host and deploy.
View or download sample code. See how to download.
Create a Web API with ASP.NET Core and Visual
Studio Code
7/30/2018 • 16 minutes to read • Edit Online
Overview
This tutorial creates the following API:
GET /api/todo Get all to-do items None Array of to-do items
The client is whatever consumes the web API (mobile app, browser, etc.). This tutorial doesn't create a
client. Postman or curl is used as the client to test the app.
A model is an object that represents the data in the app. In this case, the only model is a to-do item.
Models are represented as C# classes, also known as Plain Old CLR Object (POCOs).
A controller is an object that handles HTTP requests and creates the HTTP response. This app has a single
controller.
To keep the tutorial simple, the app doesn't use a persistent database. The sample app stores to-do items
in an in-memory database.
Prerequisites
Install the following:
.NET Core SDK 2.0 or later
Visual Studio Code
C# for Visual Studio Code
.NET Core 2.1 SDK or later
Visual Studio Code
C# for Visual Studio Code
The TodoApi folder opens in Visual Studio Code (VS Code). Select the Startup.cs file.
Select Yes to the Warn message "Required assets to build and debug are missing from 'TodoApi'. Add them?"
Select Restore to the Info message "There are unresolved dependencies".
Press Debug (F5) to build and run the program. In a browser, navigate to http://localhost:5000/api/values. The
following output is displayed:
["value1","value2"]
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>
Creating a new project in ASP.NET Core 2.0 adds the Microsoft.AspNetCore.All package reference to the
TodoApi.csproj file:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
There's no need to install the Entity Framework Core InMemory database provider separately. This database
provider allows Entity Framework Core to be used with an in-memory database.
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
}
namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoApi.Models;
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
namespace TodoApi
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddMvc();
}
Add a controller
In the Controllers folder, create a class named TodoController . Replace its contents with the following code:
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
The preceding code defines an API controller class without methods. In the next sections, methods are added to
implement the API.
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using TodoApi.Models;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
if (_context.TodoItems.Count() == 0)
{
// Create a new TodoItem if collection is empty,
// which means you can't delete all TodoItems.
_context.TodoItems.Add(new TodoItem { Name = "Item1" });
_context.SaveChanges();
}
}
}
}
In the next sections, methods are added to implement the API. The class is annotated with an [ApiController]
attribute to enable some convenient features. For information on features enabled by the attribute, see Annotate
class with ApiControllerAttribute.
The controller's constructor uses Dependency Injection to inject the database context ( TodoContext ) into the
controller. The database context is used in each of the CRUD methods in the controller. The constructor adds an
item to the in-memory database if one doesn't exist.
[HttpGet]
public ActionResult<List<TodoItem>> GetAll()
{
return _context.TodoItems.ToList();
}
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Later in the tutorial, I'll show how the HTTP response can be viewed with Postman or curl.
Routing and URL paths
The [HttpGet] attribute denotes a method that responds to an HTTP GET request. The URL path for each
method is constructed as follows:
Take the template string in the controller's Route attribute:
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoContext _context;
Replace [controller] with the name of the controller, which is the controller class name minus the
"Controller" suffix. For this sample, the controller class name is TodoController and the root name is "todo".
ASP.NET Core routing is case insensitive.
If the [HttpGet] attribute has a route template (such as [HttpGet("/products")] , append that to the path. This
sample doesn't use a template. For more information, see Attribute routing with Http[Verb] attributes.
In the following GetById method, "{id}" is a placeholder variable for the unique identifier of the to-do item.
When GetById is invoked, it assigns the value of "{id}" in the URL to the method's id parameter.
Add an HTML file, named index.html, to the project's wwwroot directory. Replace its contents with the following
markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<style>
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#spoiler {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #0066CC;
background-color: #0066CC;
color: white;
}
td {
border: 1px solid;
padding: 5px;
}
</style>
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST" onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="spoiler">
<h3>Edit</h3>
<form class="my-form">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Edit">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="site.js"></script>
</body>
</html>
Add a JavaScript file, named site.js, to the project's wwwroot directory. Replace its contents with the following
code:
$(document).ready(function () {
getData();
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$('#edit-name').val(item.name);
$('#edit-id').val(item.id);
$('#edit-isComplete')[0].checked = item.isComplete;
}
});
$('#spoiler').css({ 'display': 'block' });
}
$('.my-form').on('submit', function () {
const item = {
const item = {
'name': $('#edit-name').val(),
'isComplete': $('#edit-isComplete').is(':checked'),
'id': $('#edit-id').val()
};
$.ajax({
url: uri + '/' + $('#edit-id').val(),
type: 'PUT',
accepts: 'application/json',
contentType: 'application/json',
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$('#spoiler').css({ 'display': 'none' });
}
A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally. Open
launchSettings.json in the Properties directory of the project. Remove the launchUrl property to force the app to
open at index.html—the project's default file.
There are several ways to get jQuery. In the preceding snippet, the library is loaded from a CDN. This sample is a
complete CRUD example of calling the API with jQuery. There are additional features in this sample to make the
experience richer. Below are explanations around the calls to the API.
Get a list of to -do items
To get a list of to-do items, send an HTTP GET request to /api/todo.
The jQuery ajax function sends an AJAX request to the API, which returns JSON representing an object or array.
This function can handle all forms of HTTP interaction, sending an HTTP request to the specified url . GET is
used as the type . The success callback function is invoked if the request succeeds. In the callback, the DOM is
updated with the to-do information.
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: uri,
success: function (data) {
$('#todos').empty();
getCount(data.length);
$.each(data, function (key, item) {
const checked = item.isComplete ? 'checked' : '';
todos = data;
}
});
}
function addItem() {
const item = {
'name': $('#add-name').val(),
'isComplete': false
};
$.ajax({
type: 'POST',
accepts: 'application/json',
url: uri,
contentType: 'application/json',
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
alert('here');
},
success: function (result) {
getData();
$('#add-name').val('');
}
});
}
$.ajax({
url: uri + '/' + id,
type: 'DELETE',
success: function (result) {
getData();
}
});
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (item == null)
{
return BadRequest();
}
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. The [FromBody] attribute
tells MVC to get the value of the to-do item from the body of the HTTP request.
[HttpPost]
public IActionResult Create(TodoItem item)
{
_context.TodoItems.Add(item);
_context.SaveChanges();
The preceding code is an HTTP POST method, as indicated by the [HttpPost] attribute. MVC gets the value of the
to-do item from the body of the HTTP request.
The CreatedAtRoute method:
Returns a 201 response. 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. See 10.2.2 201 Created.
Uses the "GetTodo" named route to create the URL. The "GetTodo" named route is defined in GetById :
{
"name":"walk dog",
"isComplete":true
}
TIP
If no response displays after clicking Send, disable the SSL certification verification option. This is found under File >
Settings. Click the Send button again after disabling the setting.
Click the Headers tab in the Response pane and copy the Location header value:
The Location header URI can be used to access the new item.
Update
Add the following Update method:
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] TodoItem item)
{
if (item == null || item.Id != id)
{
return BadRequest();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
todo.IsComplete = item.IsComplete;
todo.Name = item.Name;
_context.TodoItems.Update(todo);
_context.SaveChanges();
return NoContent();
}
Update is similar to Create , 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 deltas. To support
partial updates, use HTTP PATCH.
Use Postman to update the to-do item's name to "walk cat":
Delete
Add the following Delete method:
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var todo = _context.TodoItems.Find(id);
if (todo == null)
{
return NotFound();
}
_context.TodoItems.Remove(todo);
_context.SaveChanges();
return NoContent();
}
Next steps
For information on using a persistent database, see:
Create a Razor Pages web app with ASP.NET Core
Work with data in ASP.NET Core
ASP.NET Core Web API help pages using Swagger
Routing to controller actions
Build web APIs with ASP.NET Core
Controller action return types
For information about deploying an API, including to Azure App Service, see Host and deploy.
View or download sample code. See how to download.
Develop ASP.NET Core apps using a file watcher
7/14/2018 • 3 minutes to read • Edit Online
dotnet run
The console output shows messages similar to the following (indicating that the app is running and awaiting
requests):
$ dotnet run
Hosting environment: Development
Content root path: C:/Docs/aspnetcore/tutorials/dotnet-watch/sample/WebApp
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
In a web browser, navigate to http://localhost:<port number>/api/math/sum?a=4&b=5 . You should see the result of
9 .
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
</ItemGroup>
dotnet restore
Run dotnet watch run in the WebApp folder. The console output indicates watch has started.
Save the file. The console output indicates that dotnet watch detected a file change and restarted the app.
Verify http://localhost:<port number>/api/math/product?a=4&b=5 returns the correct result.
5. Fix the Product method code so it returns the product. Save the file.
dotnet watch detects the file change and reruns the tests. The console output indicates the tests passed.
More items can be added to the watch list by editing the .csproj file. Items can be specified individually or by using
glob patterns.
<ItemGroup>
<!-- extends watching group to include *.js files -->
<Watch Include="**\*.js" Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
</ItemGroup>
<ItemGroup>
<!-- exclude Generated.cs from dotnet-watch -->
<Compile Include="Generated.cs" Watch="false" />
<Project>
<ItemGroup>
<TestProjects Include="**\*.csproj" />
<Watch Include="**\*.cs" />
</ItemGroup>
<Target Name="Test">
<MSBuild Targets="VSTest" Projects="@(TestProjects)" />
</Target>
To start file watching on both projects, change to the test folder. Execute the following command:
dotnet-watch in GitHub
dotnet-watch is part of the GitHub DotNetTools repository.
Create backend services for native mobile apps with
ASP.NET Core
9/26/2018 • 8 minutes to read • Edit Online
By Steve Smith
Mobile apps can easily communicate with ASP.NET Core backend services.
View or download sample backend services code
Features
The ToDoRest app supports listing, adding, deleting, and updating To-Do items. Each item has an ID, a Name,
Notes, and a property indicating whether it's been Done yet.
The main view of the items, as shown above, lists each item's name and indicates if it's done with a checkmark.
Tapping the + icon opens an add item dialog:
Tapping an item on the main list screen opens up an edit dialog where the item's Name, Notes, and Done settings
can be modified, or the item can be deleted:
This sample is configured by default to use backend services hosted at developer.xamarin.com, which allow read-
only operations. To test it out yourself against the ASP.NET Core app created in the next section running on your
computer, you'll need to update the app's RestUrl constant. Navigate to the ToDoREST project and open the
Constants.cs file. Replace the RestUrl with a URL that includes your machine's IP address (not localhost or
127.0.0.1, since this address is used from the device emulator, not from your machine). Include the port number as
well (5000). In order to test that your services work with a device, ensure you don't have an active firewall blocking
access to this port.
NOTE
Make sure you run the application directly, rather than behind IIS Express, which ignores non-local requests by default. Run
dotnet run from a command prompt, or choose the application name profile from the Debug Target dropdown in the Visual
Studio toolbar.
Add a model class to represent To-Do items. Mark required fields using the [Required] attribute:
using System.ComponentModel.DataAnnotations;
namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Notes { get; set; }
The API methods require some way to work with data. Use the same IToDoRepository interface the original
Xamarin sample uses:
using System.Collections.Generic;
using ToDoApi.Models;
namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}
For this sample, the implementation just uses a private collection of items:
using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;
public ToDoRepository()
{
InitializeData();
}
_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}
services.AddSingleton<IToDoRepository,ToDoRepository>();
}
TIP
Learn more about creating web APIs in Build your first Web API with ASP.NET Core MVC and Visual Studio.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;
This API supports four different HTTP verbs to perform CRUD (Create, Read, Update, Delete) operations on the
data source. The simplest of these is the Read operation, which corresponds to an HTTP GET request.
Reading Items
Requesting a list of items is done with a GET request to the List method. The [HttpGet] attribute on the List
method indicates that this action should only handle GET requests. The route for this action is the route specified
on the controller. You don't necessarily need to use the action name as part of the route. You just need to ensure
each action has a unique and unambiguous route. Routing attributes can be applied at both the controller and
method levels to build up specific routes.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}
The List method returns a 200 OK response code and all of the ToDo items, serialized as JSON.
You can test your new API method using a variety of tools, such as Postman, shown here:
Creating Items
By convention, creating new data items is mapped to the HTTP POST verb. The Create method has an
[HttpPost] attribute applied to it, and accepts a ToDoItem instance. Since the item argument will be passed in the
body of the POST, this parameter is decorated with the [FromBody] attribute.
Inside the method, the item is checked for validity and prior existence in the data store, and if no issues occur, it's
added using the repository. Checking ModelState.IsValid performs model validation, and should be done in every
API method that accepts user input.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}
The sample uses an enum containing error codes that are passed to the mobile client:
Test adding new items using Postman by choosing the POST verb providing the new object in JSON format in the
Body of the request. You should also add a request header specifying a Content-Type of application/json .
The method returns the newly created item in the response.
Updating Items
Modifying records is done using HTTP PUT requests. Other than this change, the Edit method is almost identical
to Create . Note that if the record isn't found, the Edit action will return a NotFound (404) response.
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}
To test with Postman, change the verb to PUT. Specify the updated object data in the Body of the request.
This method returns a NoContent (204) response when successful, for consistency with the pre-existing API.
Deleting Items
Deleting records is accomplished by making DELETE requests to the service, and passing the ID of the item to be
deleted. As with updates, requests for items that don't exist will receive NotFound responses. Otherwise, a
successful request will get a NoContent (204) response.
[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}
Note that when testing the delete functionality, nothing is required in the Body of the request.
Additional resources
Authentication and Authorization
ASP.NET Core fundamentals
8/20/2018 • 7 minutes to read • Edit Online
An ASP.NET Core app is a console app that creates a web server in its Main method:
The Main method invokes WebHost.CreateDefaultBuilder, which follows the builder pattern to create a web host.
The builder has methods that define the web server (for example, UseKestrel) and the startup class ( UseStartup).
In the preceding example, the Kestrel web server is automatically allocated. ASP.NET Core's web host attempts to
run on IIS, if available. Other web servers, such as HTTP.sys, can be used by invoking the appropriate extension
method. UseStartup is explained further in the next section.
IWebHostBuilder, the return type of the WebHost.CreateDefaultBuilder invocation, provides many optional
methods. Some of these methods include UseHttpSys for hosting the app in HTTP.sys and UseContentRoot for
specifying the root content directory. The Build and Run methods build the IWebHost object that hosts the app
and begins listening for HTTP requests.
host.Run();
}
}
The Main method uses WebHostBuilder, which follows the builder pattern to create a web app host. The builder
has methods that define the web server (for example, UseKestrel) and the startup class ( UseStartup). In the
preceding example, the Kestrel web server is used. Other web servers, such as WebListener, can be used by
invoking the appropriate extension method. UseStartup is explained further in the next section.
WebHostBuilder provides many optional methods, including UseIISIntegration for hosting in IIS and IIS Express
and UseContentRoot for specifying the root content directory. The Build and Run methods build the IWebHost
object that hosts the app and begins listening for HTTP requests.
Startup
The UseStartup method on WebHostBuilder specifies the Startup class for your app:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
host.Run();
}
}
The Startup class is where you define the request handling pipeline and where any services needed by the app
are configured. The Startup class must be public and contain the following methods:
ConfigureServices defines the Services used by your app (for example, ASP.NET Core MVC, Entity Framework
Core, Identity). Configure defines the middleware called in the request pipeline.
For more information, see Application startup in ASP.NET Core.
Content root
The content root is the base path to any content used by the app, such as Razor Pages, MVC views, and static
assets. By default, the content root is the same location as the app base path for the executable hosting the app.
Web root
The web root of an app is the directory in the project containing public, static resources, such as CSS, JavaScript,
and image files.
Middleware
In ASP.NET Core, you compose your request pipeline using middleware. ASP.NET Core middleware performs
asynchronous operations on an HttpContext and then either invokes the next middleware in the pipeline or
terminates the request.
By convention, a middleware component called "XYZ" is added to the pipeline by invoking a UseXYZ extension
method in the Configure method.
ASP.NET Core includes a rich set of built-in middleware, and you can write your own custom middleware. Open
Web Interface for .NET (OWIN ), which allows web apps to be decoupled from web servers, is supported in
ASP.NET Core apps.
For more information, see ASP.NET Core Middleware and Open Web Interface for .NET (OWIN ) with ASP.NET
Core.
Environments
Environments, such as Development and Production, are a first-class notion in ASP.NET Core and can be set
using an environment variable, settings file, and command-line argument.
For more information, see Use multiple environments in ASP.NET Core.
Hosting
ASP.NET Core apps configure and launch a host, which is responsible for app startup and lifetime management.
For more information, see Host in ASP.NET Core.
Servers
The ASP.NET Core hosting model doesn't directly listen for requests. The hosting model relies on an HTTP server
implementation to forward the request to the app. The forwarded request is wrapped as a set of feature objects
that can be accessed through interfaces. ASP.NET Core includes a managed, cross-platform web server, called
Kestrel. Kestrel is commonly run behind a production web server, such as IIS or Nginx in a reverse proxy
configuration. Kestrel can also be run as an edge server exposed directly to the Internet in ASP.NET Core 2.0 or
later.
For more information, see Web server implementations in ASP.NET Core.
Configuration
ASP.NET Core uses a configuration model based on name-value pairs. The configuration model isn't based on
System.Configuration or web.config. Configuration obtains settings from an ordered set of configuration
providers. The built-in configuration providers support a variety of file formats (XML, JSON, INI), environment
variables, and command-line arguments. You can also write your own custom configuration providers.
For more information, see Configuration in ASP.NET Core.
Logging
ASP.NET Core supports a Logging API that works with a variety of logging providers. Built-in providers support
sending logs to one or more destinations. Third-party logging frameworks can be used.
For more information, see Logging in ASP.NET Core.
Error handling
ASP.NET Core has built-in scenarios for handling errors in apps, including a developer exception page, custom
error pages, static status code pages, and startup exception handling.
For more information, see Handle errors in ASP.NET Core.
Routing
ASP.NET Core offers scenarios for routing of app requests to route handlers.
For more information, see Routing in ASP.NET Core.
File Providers
ASP.NET Core abstracts file system access through the use of File Providers, which offers a common interface for
working with files across platforms.
For more information, see File Providers in ASP.NET Core.
Static files
Static Files Middleware serves static files, such as HTML, CSS, image, and JavaScript files.
For more information, see Static files in ASP.NET Core.
Request features
Web server implementation details related to HTTP requests and responses are defined in interfaces. These
interfaces are used by server implementations and middleware to create and modify the app's hosting pipeline.
For more information, see Request Features in ASP.NET Core.
Background tasks
Background tasks are implemented as hosted services. A hosted service is a class with background task logic that
implements the IHostedService interface.
For more information, see Background tasks with hosted services in ASP.NET Core.
Access HttpContext
HttpContext is automatically available when processing requests with Razor Pages and MVC. In circumstances
where HttpContext isn't readily available, you can access the HttpContext through the IHttpContextAccessor
interface and its default implementation, HttpContextAccessor.
For more information, see Access HttpContext in ASP.NET Core.
WebSockets
WebSocket is a protocol that enables two-way persistent communication channels over TCP connections. It's used
for apps such as chat, stock tickers, games, and anywhere you desire real-time functionality in a web app.
ASP.NET Core supports web socket scenarios.
For more information, see WebSockets support in ASP.NET Core.
Microsoft.AspNetCore.App metapackage
The Microsoft.AspNetCore.App metapackage simplifies package management.
For more information, see Microsoft.AspNetCore.App metapackage for ASP.NET Core 2.1 and later.
Microsoft.AspNetCore.All metapackage
The Microsoft.AspNetCore.All metapackage for ASP.NET Core includes:
All supported packages by the ASP.NET Core team.
All supported packages by Entity Framework Core.
Internal and 3rd-party dependencies used by ASP.NET Core and Entity Framework Core.
For more information, see Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.0.
The web host provides some services that are available to the Startup class constructor. The app adds
additional services via ConfigureServices . Both the host and app services are then available in Configure and
throughout the app.
A common use of dependency injection into the Startup class is to inject:
IHostingEnvironment to configure services by environment.
IConfiguration to read configuration.
ILoggerFactory to create a logger in Startup.ConfigureServices .
public class Startup
{
private readonly IHostingEnvironment _env;
private readonly IConfiguration _config;
private readonly ILoggerFactory _loggerFactory;
if (_env.IsDevelopment())
{
// Development service configuration
logger.LogInformation("Development environment");
}
else
{
// Non-development service configuration
logger.LogInformation($"Environment: {_env.EnvironmentName}");
}
An alternative to injecting IHostingEnvironment is to use a conventions-based approach. The app can define
separate Startup classes for different environments (for example, StartupDevelopment ), and the appropriate
Startup class is selected at runtime. The class whose name suffix matches the current environment is
prioritized. If the app is run in the Development environment and includes both a Startup class and a
StartupDevelopment class, the StartupDevelopment class is used. For more information, see Use multiple
environments.
To learn more about WebHostBuilder , see the Hosting topic. For information on handling errors during startup,
see Startup exception handling.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
The ASP.NET Core templates configure the pipeline with support for a developer exception page, BrowserLink,
error pages, static files, and ASP.NET Core MVC:
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
Each Use extension method adds a middleware component to the request pipeline. For instance, the UseMvc
extension method adds the Routing Middleware to the request pipeline and configures MVC as the default
handler.
Each middleware component in the request pipeline is responsible for invoking the next component in the
pipeline or short-circuiting the chain, if appropriate. If short-circuiting doesn't occur along the middleware chain,
each middleware has a second chance to process the request before it's sent to the client.
Additional services, such as IHostingEnvironment and ILoggerFactory , may also be specified in the method
signature. When specified, additional services are injected if they're available.
For more information on how to use IApplicationBuilder and the order of middleware processing, see
Middleware.
Convenience methods
ConfigureServices and Configure convenience methods can be used instead of specifying a Startup class.
Multiple calls to ConfigureServices append to one another. Multiple calls to Configure use the last method call.
if (HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseMvcWithDefaultRoute();
app.UseStaticFiles();
});
}
public RequestSetOptionsMiddleware(
RequestDelegate next, IOptions<AppOptions> injectedOptions)
{
_next = next;
_injectedOptions = injectedOptions;
}
if (!string.IsNullOrWhiteSpace(option))
{
_injectedOptions.Value.Option = WebUtility.HtmlEncode(option);
}
await _next(httpContext);
}
}
When a query string parameter for option is provided, the middleware processes the value assignment before
the MVC middleware renders the response:
Additional resources
Host in ASP.NET Core
Use multiple environments in ASP.NET Core
ASP.NET Core Middleware
Logging in ASP.NET Core
Configuration in ASP.NET Core
Dependency injection in ASP.NET Core
9/26/2018 • 17 minutes to read • Edit Online
return Task.FromResult(0);
}
}
An instance of the MyDependency class can be created to make the WriteMessage method
available to a class. The MyDependency class is a dependency of the IndexModel class:
An instance of the MyDependency class can be created to make the WriteMessage method
available to a class. The MyDependency class is a dependency of the HomeController class:
public class HomeController : Controller
{
MyDependency _dependency = new MyDependency();
return View();
}
}
The class creates and directly depends on the MyDependency instance. Code dependencies (such
as the previous example) are problematic and should be avoided for the following reasons:
To replace MyDependency with a different implementation, the class must be modified.
If MyDependency has dependencies, they must be configured by the class. In a large project
with multiple classes depending on MyDependency , the configuration code becomes
scattered across the app.
This implementation is difficult to unit test. The app should use a mock or stub
MyDependency class, which isn't possible with this approach.
return Task.FromResult(0);
}
}
return Task.FromResult(0);
}
}
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
NOTE
Each services.Add{SERVICE_NAME} extension method adds (and potentially configures) services. For
example, services.AddMvc() adds the services Razor Pages and MVC require. We recommended
that apps follow this convention. Place extension methods in the
Microsoft.Extensions.DependencyInjection namespace to encapsulate groups of service registrations.
If the service's constructor requires a primitive, such as a string , the primitive can be injected
by using configuration or the options pattern:
// Use myStringValue
}
...
}
An instance of the service is requested via the constructor of a class where the service is used
and assigned to a private field. The field is used to access the service as necessary throughout
the class.
In the sample app, the IMyDependency instance is requested and used to call the service's
WriteMessage method:
public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}
// GET: /mydependency/
public async Task<IActionResult> Index()
{
await _myDependency.WriteMessage(
"MyDependencyController.Index created this message.");
return View();
}
}
Framework-provided services
The Startup.ConfigureServices method is responsible for defining the services the app uses,
including platform features, such as Entity Framework Core and ASP.NET Core MVC. Initially,
the IServiceCollection provided to ConfigureServices has the following services defined
(depending on how the host was configured):
SERVICE TYPE LIFETIME
Microsoft.AspNetCore.Hosting.Builder.IApplication Transient
BuilderFactory
Microsoft.AspNetCore.Hosting.IApplicationLifetime Singleton
Microsoft.AspNetCore.Hosting.IHostingEnvironme Singleton
nt
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transient
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transient
Microsoft.Extensions.Logging.ILogger<T> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvide Singleton
r
Microsoft.Extensions.Options.IConfigureOptions<T Transient
>
Microsoft.Extensions.Options.IOptions<T> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton
When a service collection extension method is available to register a service (and its dependent
services, if required), the convention is to use a single Add{SERVICE_NAME} extension method to
register all of the services required by that service. The following code is an example of how to
add additional services to the container using the extension methods AddDbContext,
AddIdentity, and AddMvc:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
For more information, see the ServiceCollection Class in the API documentation.
Service lifetimes
Choose an appropriate lifetime for each registered service. ASP.NET Core services can be
configured with the following lifetimes:
Transient
Transient lifetime services are created each time they're requested. This lifetime works best for
lightweight, stateless services.
Scoped
Scoped lifetime services are created once per request.
WARNING
When using a scoped service in a middleware, inject the service into the Invoke or InvokeAsync
method. Don't inject via constructor injection because it forces the service to behave like a singleton.
For more information, see ASP.NET Core Middleware.
Singleton
Singleton lifetime services are created the first time they're requested (or when
ConfigureServices is run and an instance is specified with the service registration). Every
subsequent request uses the same instance. If the app requires singleton behavior, allowing the
service container to manage the service's lifetime is recommended. Don't implement the
singleton design pattern and provide user code to manage the object's lifetime in the class.
WARNING
It's dangerous to resolve a scoped service from a singleton. It may cause the service to have incorrect
state when processing subsequent requests.
The interfaces are implemented in the Operation class. The Operation constructor generates a
GUID if one isn't supplied:
public class Operation : IOperationTransient,
IOperationScoped,
IOperationSingleton,
IOperationSingletonInstance
{
public Operation() : this(Guid.NewGuid())
{
}
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
The sample app demonstrates object lifetimes within and between individual requests. The
sample app's IndexModel requests each kind of IOperation type and the OperationService .
The page then displays all of the page model class's and service's OperationId values through
property assignments:
public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}
The sample app demonstrates object lifetimes within and between individual requests. The
sample app includes an OperationsController that requests each kind of IOperation type and
the OperationService . The Index action sets the services into the ViewBag for display of the
service's OperationId values:
public class OperationsController : Controller
{
private readonly OperationService _operationService;
private readonly IOperationTransient _transientOperation;
private readonly IOperationScoped _scopedOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationSingletonInstance _singletonInstanceOperation;
public OperationsController(
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_operationService = operationService;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
_singletonInstanceOperation = singletonInstanceOperation;
}
return View();
}
}
try
{
var serviceContext = services.GetRequiredService<MyScopedService>();
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
}
host.Run();
}
Scope validation
When the app is running in the Development environment, the default service provider
performs checks to verify that:
Scoped services aren't directly or indirectly resolved from the root service provider.
Scoped services aren't directly or indirectly injected into singletons.
The root service provider is created when BuildServiceProvider is called. The root service
provider's lifetime corresponds to the app/server's lifetime when the provider starts with the
app and is disposed when the app shuts down.
Scoped services are disposed by the container that created them. If a scoped service is created
in the root container, the service's lifetime is effectively promoted to singleton because it's only
disposed by the root container when app/server is shut down. Validating service scopes
catches these situations when BuildServiceProvider is called.
For more information, see ASP.NET Core Web Host.
Request Services
The services available within an ASP.NET Core request from HttpContext are exposed
through the HttpContext.RequestServices collection.
Request Services represent the services configured and requested as part of the app. When the
objects specify dependencies, these are satisfied by the types found in RequestServices , not
ApplicationServices .
Generally, the app shouldn't use these properties directly. Instead, request the types that
classes require via class constructors and allow the framework inject the dependencies. This
yields classes that are easier to test (see the Test and debug topics).
NOTE
Prefer requesting dependencies as constructor parameters to accessing the RequestServices
collection.
NOTE
In ASP.NET Core 1.0, the container calls dispose on all IDisposable objects, including those it didn't
create.
See the Dependency Injection readme.md file for a list of some of the containers that support
adapters.
The following sample replaces the built-in container with Autofac:
Install the appropriate container package(s):
Autofac
Autofac.Extensions.DependencyInjection
Configure the container in Startup.ConfigureServices and return an IServiceProvider :
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Add other framework services
// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<DefaultModule>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
At runtime, Autofac is used to resolve types and inject dependencies. To learn more about
using Autofac with ASP.NET Core, see the Autofac documentation.
Thread safety
Singleton services need to be thread safe. If a singleton service has a dependency on a
transient service, the transient service may also need to be thread safe depending how it's used
by the singleton.
The factory method of single service, such as the second argument to
AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), doesn't need
to be thread-safe. Like a type ( static ) constructor, it's guaranteed to be called once by a single
thread.
Recommendations
When working with dependency injection, keep the following recommendations in mind:
Avoid storing data and configuration directly in the service container. For example, a
user's shopping cart shouldn't typically be added to the service container. Configuration
should use the options pattern. Similarly, avoid "data holder" objects that only exist to
allow access to some other object. It's better to request the actual item via dependency
injection, if possible.
Avoid static access to services (for example, statically-typing
IApplicationBuilder.ApplicationServices for use elsewhere).
Avoid using the service locator pattern (for example, IServiceProvider.GetService).
Avoid static access to HttpContext (for example, IHttpContextAccessor.HttpContext).
Like all sets of recommendations, you may encounter situations where ignoring a
recommendation is required. Exceptions are rare—mostly special cases within the framework
itself.
Dependency injection is an alternative to static/global object access patterns. You may not be
able to realize the benefits of dependency injection if you mix it with static object access.
Additional resources
Dependency injection into views in ASP.NET Core
Dependency injection into controllers in ASP.NET Core
Dependency injection in requirement handlers in ASP.NET Core
Repository pattern with ASP.NET Core
Application startup in ASP.NET Core
Test, debug, and troubleshoot in ASP.NET Core
Factory-based middleware activation in ASP.NET Core
Writing Clean Code in ASP.NET Core with Dependency Injection (MSDN )
Container-Managed Application Design, Prelude: Where does the Container Belong?
Explicit Dependencies Principle
Inversion of Control Containers and the Dependency Injection Pattern (Martin Fowler)
New is Glue ("gluing" code to a particular implementation)
How to register a service with multiple interfaces in ASP.NET Core DI
ASP.NET Core Middleware
9/20/2018 • 11 minutes to read • Edit Online
Each delegate can perform operations before and after the next delegate. A delegate can also decide to not pass a
request to the next delegate, which is called short-circuiting the request pipeline. Short-circuiting is often desirable
because it avoids unnecessary work. For example, Static Files Middleware can return a request for a static file and
short-circuit the rest of the pipeline. Exception-handling delegates are called early in the pipeline, so they can catch
exceptions that occur in later stages of the pipeline.
The simplest possible ASP.NET Core app sets up a single request delegate that handles all requests. This case
doesn't include an actual request pipeline. Instead, a single anonymous function is called in response to every HTTP
request.
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}
WARNING
Don't call next.Invoke after the response has been sent to the client. Changes to HttpResponse after the response has
started throw an exception. For example, changes such as setting headers and a status code throw an exception. Writing to
the response body after calling next :
May cause a protocol violation. For example, writing more than the stated Content-Length .
May corrupt the body format. For example, writing an HTML footer to a CSS file.
HasStarted is a useful hint to indicate if headers have been sent or the body has been written to.
Order
The order that middleware components are added in the Startup.Configure method defines the order in which the
middleware components are invoked on requests and the reverse order for the response. The order is critical for
security, performance, and functionality.
The following Startup.Configure method adds middleware components for common app scenarios:
1. Exception/error handling
2. HTTP Strict Transport Security Protocol
3. HTTPS redirection
4. Static file server
5. Cookie policy enforcement
6. Authentication
7. Session
8. MVC
// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
app.UseSession();
1. Exception/error handling
2. Static files
3. Authentication
4. Session
5. MVC
public void Configure(IApplicationBuilder app)
{
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
app.UseExceptionHandler("/Home/Error");
In the preceding example code, each middleware extension method is exposed on IApplicationBuilder through the
Microsoft.AspNetCore.Builder namespace.
UseExceptionHandler is the first middleware component added to the pipeline. Therefore, the Exception Handler
Middleware catches any exceptions that occur in later calls.
Static Files Middleware is called early in the pipeline so that it can handle requests and short-circuit without going
through the remaining components. The Static Files Middleware provides no authorization checks. Any files served
by it, including those under wwwroot, are publicly available. For an approach to secure static files, see Static files in
ASP.NET Core.
If the request isn't handled by the Static Files Middleware, it's passed on to the Authentication Middleware
(UseAuthentication), which performs authentication. Authentication doesn't short-circuit unauthenticated requests.
Although Authentication Middleware authenticates requests, authorization (and rejection) occurs only after MVC
selects a specific Razor Page or MVC controller and action.
If the request isn't handled by Static Files Middleware, it's passed on to the Identity Middleware (UseIdentity),
which performs authentication. Identity doesn't short-circuit unauthenticated requests. Although Identity
authenticates requests, authorization (and rejection) occurs only after MVC selects a specific controller and action.
The following example demonstrates a middleware order where requests for static files are handled by Static Files
Middleware before Response Compression Middleware. Static files aren't compressed with this middleware order.
The MVC responses from UseMvcWithDefaultRoute can be compressed.
Map extensions are used as a convention for branching the pipeline. Map* branches the request pipeline based on
matches of the given request path. If the request path starts with the given path, the branch is executed.
public class Startup
{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
app.Map("/map2", HandleMapTest2);
The following table shows the requests and responses from http://localhost:1234 using the previous code.
REQUEST RESPONSE
When Mapis used, the matched path segment(s) are removed from HttpRequest.Path and appended to
HttpRequest.PathBase for each request.
MapWhen branches the request pipeline based on the result of the given predicate. Any predicate of type
Func<HttpContext, bool> can be used to map requests to a new branch of the pipeline. In the following example, a
predicate is used to detect the presence of a query string variable branch :
public class Startup
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
The following table shows the requests and responses from http://localhost:1234 using the previous code.
REQUEST RESPONSE
Built-in middleware
ASP.NET Core ships with the following middleware components. The Order column provides notes on the
middleware's placement in the request pipeline and under what conditions the middleware may terminate the
request and prevent other middleware from processing a request.
Cookie Policy Tracks consent from users for storing Before middleware that issues cookies.
personal information and enforces Examples: Authentication, Session, MVC
minimum standards for cookie fields, (TempData).
such as secure and SameSite .
Forwarded Headers Forwards proxied headers onto the Before components that consume the
current request. updated fields. Examples: scheme, host,
client IP, method.
HTTP Method Override Allows an incoming POST request to Before components that consume the
override the method. updated method.
HTTPS Redirection Redirect all HTTP requests to HTTPS Before components that consume the
(ASP.NET Core 2.1 or later). URL.
HTTP Strict Transport Security (HSTS) Security enhancement middleware that Before responses are sent and after
adds a special response header components that modify requests.
(ASP.NET Core 2.1 or later). Examples: Forwarded Headers, URL
Rewriting.
MIDDLEWARE DESCRIPTION ORDER
OWIN Interop with OWIN-based apps, servers, Terminal if the OWIN Middleware fully
and middleware. processes the request.
Response Caching Provides support for caching responses. Before components that require
caching.
Response Compression Provides support for compressing Before components that require
responses. compression.
Routing Defines and constrains request routes. Terminal for matching routes.
Session Provides support for managing user Before components that require Session.
sessions.
Static Files Provides support for serving static files Terminal if a request matches a file.
and directory browsing.
URL Rewriting Provides support for rewriting URLs and Before components that consume the
redirecting requests. URL.
WebSockets Enables the WebSockets protocol. Before components that are required to
accept WebSocket requests.
Write middleware
Middleware is generally encapsulated in a class and exposed with an extension method. Consider the following
middleware, which sets the culture for the current request from a query string:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
}
}
The preceding sample code is used to demonstrate creating a middleware component. For ASP.NET Core's built-in
localization support, see Globalization and localization in ASP.NET Core.
You can test the middleware by passing in the culture, for example http://localhost:7997/?culture=no .
The following code moves the middleware delegate to a class:
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
The middleware Task method's name must be Invoke . In ASP.NET Core 2.0 or later, the name can be either
Invoke or InvokeAsync .
using Microsoft.AspNetCore.Builder;
namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
}
}
}
Middleware should follow the Explicit Dependencies Principle by exposing its dependencies in its constructor.
Middleware is constructed once per application lifetime. See the Per-request dependencies section if you need to
share services with middleware within a request.
Middleware components can resolve their dependencies from dependency injection (DI) through constructor
parameters. UseMiddleware<T> can also accept additional parameters directly.
Per-request dependencies
Because middleware is constructed at app startup, not per-request, scoped lifetime services used by middleware
constructors aren't shared with other dependency-injected types during each request. If you must share a scoped
service between your middleware and other types, add these services to the Invoke method's signature. The
Invoke method can accept additional parameters that are populated by DI:
Additional resources
Migrate HTTP handlers and modules to ASP.NET Core middleware
Application startup in ASP.NET Core
Request Features in ASP.NET Core
Factory-based middleware activation in ASP.NET Core
Middleware activation with a third-party container in ASP.NET Core
ASP.NET Core Middleware
9/20/2018 • 11 minutes to read • Edit Online
Each delegate can perform operations before and after the next delegate. A delegate can also decide to not
pass a request to the next delegate, which is called short-circuiting the request pipeline. Short-circuiting is
often desirable because it avoids unnecessary work. For example, Static Files Middleware can return a
request for a static file and short-circuit the rest of the pipeline. Exception-handling delegates are called
early in the pipeline, so they can catch exceptions that occur in later stages of the pipeline.
The simplest possible ASP.NET Core app sets up a single request delegate that handles all requests. This
case doesn't include an actual request pipeline. Instead, a single anonymous function is called in response to
every HTTP request.
WARNING
Don't call next.Invoke after the response has been sent to the client. Changes to HttpResponse after the response
has started throw an exception. For example, changes such as setting headers and a status code throw an exception.
Writing to the response body after calling next :
May cause a protocol violation. For example, writing more than the stated Content-Length .
May corrupt the body format. For example, writing an HTML footer to a CSS file.
HasStarted is a useful hint to indicate if headers have been sent or the body has been written to.
Order
The order that middleware components are added in the Startup.Configure method defines the order in
which the middleware components are invoked on requests and the reverse order for the response. The
order is critical for security, performance, and functionality.
The following Startup.Configure method adds middleware components for common app scenarios:
1. Exception/error handling
2. HTTP Strict Transport Security Protocol
3. HTTPS redirection
4. Static file server
5. Cookie policy enforcement
6. Authentication
7. Session
8. MVC
// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
app.UseSession();
1. Exception/error handling
2. Static files
3. Authentication
4. Session
5. MVC
public void Configure(IApplicationBuilder app)
{
// Enable the Exception Handler Middleware to catch exceptions
// thrown in the following middlewares.
app.UseExceptionHandler("/Home/Error");
In the preceding example code, each middleware extension method is exposed on IApplicationBuilder
through the Microsoft.AspNetCore.Builder namespace.
UseExceptionHandler is the first middleware component added to the pipeline. Therefore, the Exception
Handler Middleware catches any exceptions that occur in later calls.
Static Files Middleware is called early in the pipeline so that it can handle requests and short-circuit without
going through the remaining components. The Static Files Middleware provides no authorization checks.
Any files served by it, including those under wwwroot, are publicly available. For an approach to secure
static files, see Static files in ASP.NET Core.
If the request isn't handled by the Static Files Middleware, it's passed on to the Authentication Middleware
(UseAuthentication), which performs authentication. Authentication doesn't short-circuit unauthenticated
requests. Although Authentication Middleware authenticates requests, authorization (and rejection) occurs
only after MVC selects a specific Razor Page or MVC controller and action.
If the request isn't handled by Static Files Middleware, it's passed on to the Identity Middleware
(UseIdentity), which performs authentication. Identity doesn't short-circuit unauthenticated requests.
Although Identity authenticates requests, authorization (and rejection) occurs only after MVC selects a
specific controller and action.
The following example demonstrates a middleware order where requests for static files are handled by
Static Files Middleware before Response Compression Middleware. Static files aren't compressed with this
middleware order. The MVC responses from UseMvcWithDefaultRoute can be compressed.
app.Map("/map2", HandleMapTest2);
The following table shows the requests and responses from http://localhost:1234 using the previous
code.
REQUEST RESPONSE
When Mapis used, the matched path segment(s) are removed from HttpRequest.Path and appended to
HttpRequest.PathBase for each request.
MapWhen branches the request pipeline based on the result of the given predicate. Any predicate of type
Func<HttpContext, bool> can be used to map requests to a new branch of the pipeline. In the following
example, a predicate is used to detect the presence of a query string variable branch :
public class Startup
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
The following table shows the requests and responses from http://localhost:1234 using the previous
code.
REQUEST RESPONSE
Built-in middleware
ASP.NET Core ships with the following middleware components. The Order column provides notes on the
middleware's placement in the request pipeline and under what conditions the middleware may terminate
the request and prevent other middleware from processing a request.
Cookie Policy Tracks consent from users for storing Before middleware that issues
personal information and enforces cookies. Examples: Authentication,
minimum standards for cookie fields, Session, MVC (TempData).
such as secure and SameSite .
Forwarded Headers Forwards proxied headers onto the Before components that consume
current request. the updated fields. Examples: scheme,
host, client IP, method.
HTTP Method Override Allows an incoming POST request to Before components that consume
override the method. the updated method.
HTTPS Redirection Redirect all HTTP requests to HTTPS Before components that consume
(ASP.NET Core 2.1 or later). the URL.
HTTP Strict Transport Security (HSTS) Security enhancement middleware Before responses are sent and after
that adds a special response header components that modify requests.
(ASP.NET Core 2.1 or later). Examples: Forwarded Headers, URL
Rewriting.
MIDDLEWARE DESCRIPTION ORDER
Response Caching Provides support for caching Before components that require
responses. caching.
Response Compression Provides support for compressing Before components that require
responses. compression.
Session Provides support for managing user Before components that require
sessions. Session.
Static Files Provides support for serving static Terminal if a request matches a file.
files and directory browsing.
URL Rewriting Provides support for rewriting URLs Before components that consume
and redirecting requests. the URL.
WebSockets Enables the WebSockets protocol. Before components that are required
to accept WebSocket requests.
Write middleware
Middleware is generally encapsulated in a class and exposed with an extension method. Consider the
following middleware, which sets the culture for the current request from a query string:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
}
}
The preceding sample code is used to demonstrate creating a middleware component. For ASP.NET Core's
built-in localization support, see Globalization and localization in ASP.NET Core.
You can test the middleware by passing in the culture, for example http://localhost:7997/?culture=no .
The following code moves the middleware delegate to a class:
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
The middleware Task method's name must be Invoke . In ASP.NET Core 2.0 or later, the name can be
either Invoke or InvokeAsync .
The following extension method exposes the middleware through IApplicationBuilder:
using Microsoft.AspNetCore.Builder;
namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
}
}
}
Middleware should follow the Explicit Dependencies Principle by exposing its dependencies in its
constructor. Middleware is constructed once per application lifetime. See the Per-request dependencies
section if you need to share services with middleware within a request.
Middleware components can resolve their dependencies from dependency injection (DI) through
constructor parameters. UseMiddleware<T> can also accept additional parameters directly.
Per-request dependencies
Because middleware is constructed at app startup, not per-request, scoped lifetime services used by
middleware constructors aren't shared with other dependency-injected types during each request. If you
must share a scoped service between your middleware and other types, add these services to the Invoke
method's signature. The Invoke method can accept additional parameters that are populated by DI:
Additional resources
Migrate HTTP handlers and modules to ASP.NET Core middleware
Application startup in ASP.NET Core
Request Features in ASP.NET Core
Factory-based middleware activation in ASP.NET Core
Middleware activation with a third-party container in ASP.NET Core
Factory-based middleware activation in ASP.NET
Core
8/14/2018 • 2 minutes to read • Edit Online
By Luke Latham
IMiddlewareFactory/IMiddleware is an extensibility point for middleware activation.
UseMiddleware extension methods check if a middleware's registered type implements IMiddleware . If it does, the
IMiddlewareFactory instance registered in the container is used to resolve the IMiddleware implementation
instead of using the convention-based middleware activation logic. The middleware is registered as a scoped or
transient service in the app's service container.
Benefits:
Activation per request (injection of scoped services)
Strong typing of middleware
IMiddleware is activated per request, so scoped services can be injected into the middleware's constructor.
View or download sample code (how to download)
The sample app demonstrates middleware activated by:
Convention. For more information on conventional middleware activation, see the Middleware topic.
An IMiddleware implementation. The default MiddlewareFactory class activates the middleware.
The middleware implementations function identically and record the value provided by a query string parameter (
key ). The middlewares use an injected database context (a scoped service) to record the query string value in an
in-memory database.
IMiddleware
IMiddleware defines middleware for the app's request pipeline. The InvokeAsync(HttpContext, RequestDelegate)
method handles requests and returns a Task that represents the execution of the middleware.
Middleware activated by convention:
public class ConventionalMiddleware
{
private readonly RequestDelegate _next;
if (!string.IsNullOrWhiteSpace(keyValue))
{
db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "ConventionalMiddleware",
Value = keyValue
});
await db.SaveChangesAsync();
}
await _next(context);
}
}
if (!string.IsNullOrWhiteSpace(keyValue))
{
_db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "FactoryActivatedMiddleware",
Value = keyValue
});
await _db.SaveChangesAsync();
}
await next(context);
}
}
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("InMemoryDb"));
services.AddTransient<FactoryActivatedMiddleware>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
app.UseConventionalMiddleware();
app.UseFactoryActivatedMiddleware();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
IMiddlewareFactory
IMiddlewareFactory provides methods to create middleware. The middleware factory implementation is
registered in the container as a scoped service.
The default IMiddlewareFactory implementation, MiddlewareFactory, is found in the Microsoft.AspNetCore.Http
package.
Additional resources
ASP.NET Core Middleware
Middleware activation with a third-party container in ASP.NET Core
Middleware activation with a third-party container in
ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
By Luke Latham
This article demonstrates how to use IMiddlewareFactory and IMiddleware as an extensibility point for
middleware activation with a third-party container. For introductory information on IMiddlewareFactory and
IMiddleware , see the Factory-based middleware activation topic.
The sample's middleware implementation records the value provided by a query string parameter ( key ). The
middleware uses an injected database context (a scoped service) to record the query string value in an in-memory
database.
NOTE
The sample app uses Simple Injector purely for demonstration purposes. Use of Simple Injector isn't an endorsement.
Middleware activation approaches described in the Simple Injector documentation and GitHub issues are recommended by
the maintainers of Simple Injector. For more information, see the Simple Injector documentation and Simple Injector GitHub
repository.
IMiddlewareFactory
IMiddlewareFactory provides methods to create middleware.
In the sample app, a middleware factory is implemented to create an SimpleInjectorActivatedMiddleware instance.
The middleware factory uses the Simple Injector container to resolve the middleware:
if (!string.IsNullOrWhiteSpace(keyValue))
{
_db.Add(new Request()
{
DT = DateTime.UtcNow,
MiddlewareActivation = "SimpleInjectorActivatedMiddleware",
Value = keyValue
});
await _db.SaveChangesAsync();
}
await next(context);
}
}
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
_container.Register<AppDbContext>(() =>
{
var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
optionsBuilder.UseInMemoryDatabase("InMemoryDb");
return new AppDbContext(optionsBuilder.Options);
}, Lifestyle.Scoped);
_container.Register<SimpleInjectorActivatedMiddleware>();
_container.Verify();
}
app.UseSimpleInjectorActivatedMiddleware();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
Additional resources
Middleware
Factory-based middleware activation
Simple Injector GitHub repository
Simple Injector documentation
Static files in ASP.NET Core
8/14/2018 • 8 minutes to read • Edit Online
Static files are accessible via a path relative to the web root. For example, the Web Application project
template contains several folders within the wwwroot folder:
wwwroot
css
images
js
The URI format to access a file in the images subfolder is
http://<server_address>/images/<image_file_name>. For example,
http://localhost:9189/images/banner3.svg.
ASP.NET Core 2.x
ASP.NET Core 1.x
If targeting .NET Framework, add the Microsoft.AspNetCore.StaticFiles package to your project. If targeting
.NET Core, the Microsoft.AspNetCore.All metapackage includes this package.
Configure the middleware which enables the serving of static files.
Serve files inside of web root
Invoke the UseStaticFiles method within Startup.Configure :
The parameterless UseStaticFiles method overload marks the files in web root as servable. The following
markup references wwwroot/images/banner1.svg:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
RequestPath = "/StaticFiles"
});
}
In the preceding code, the MyStaticFiles directory hierarchy is exposed publicly via the StaticFiles URI
segment. A request to http://<server_address>/StaticFiles/images/banner1.svg serves the banner1.svg file.
The following markup references MyStaticFiles/images/banner1.svg:
[Authorize]
public IActionResult BannerImage()
{
var file = Path.Combine(Directory.GetCurrentDirectory(),
"MyStaticFiles", "images", "banner1.svg");
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
}
The preceding code allows directory browsing of the wwwroot/images folder using the URL
http://<server_address>/MyImages, with links to each file and folder:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
}
IMPORTANT
UseDefaultFiles must be called before UseStaticFiles to serve the default file. UseDefaultFiles is a URL
rewriter that doesn't actually serve the file. Enable the static file middleware via UseStaticFiles to serve the file.
app.UseFileServer();
The following code builds upon the parameterless overload by enabling directory browsing:
app.UseFileServer(enableDirectoryBrowsing: true);
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "MyStaticFiles")),
RequestPath = "/StaticFiles",
EnableDirectoryBrowsing = true
});
}
Using the file hierarchy and preceding code, URLs resolve as follows:
URI RESPONSE
http://<server_address>/StaticFiles/images/banner1.svg MyStaticFiles/images/banner1.svg
http://<server_address>/StaticFiles MyStaticFiles/default.html
NOTE
UseDefaultFiles and UseDirectoryBrowser use the URL http://<server_address>/StaticFiles without the trailing
slash to trigger a client-side redirect to http://<server_address>/StaticFiles/. Notice the addition of the trailing slash.
Relative URLs within the documents are deemed invalid without a trailing slash.
FileExtensionContentTypeProvider
The FileExtensionContentTypeProvider class contains a Mappings property serving as a mapping of file
extensions to MIME content types. In the following sample, several file extensions are registered to known
MIME types. The .rtf extension is replaced, and .mp4 is removed.
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages",
ContentTypeProvider = provider
});
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")),
RequestPath = "/MyImages"
});
}
With the preceding code, a request for a file with an unknown content type is returned as an image.
WARNING
Enabling ServeUnknownFileTypes is a security risk. It's disabled by default, and its use is discouraged.
FileExtensionContentTypeProvider provides a safer alternative to serving files with non-standard extensions.
Considerations
WARNING
UseDirectoryBrowser and UseStaticFiles can leak secrets. Disabling directory browsing in production is highly
recommended. Carefully review which directories are enabled via UseStaticFiles or UseDirectoryBrowser . The
entire directory and its sub-directories become publicly accessible. Store files suitable for serving to the public in a
dedicated directory, such as <content_root>/wwwroot. Separate these files from MVC views, Razor Pages (2.x only),
configuration files, etc.
The URLs for content exposed with UseDirectoryBrowser and UseStaticFiles are subject to the case
sensitivity and character restrictions of the underlying file system. For example, Windows is case
insensitive—macOS and Linux aren't.
ASP.NET Core apps hosted in IIS use the ASP.NET Core Module to forward all requests to the app,
including static file requests. The IIS static file handler isn't used. It has no chance to handle requests
before they're handled by the module.
Complete the following steps in IIS Manager to remove the IIS static file handler at the server or
website level:
1. Navigate to the Modules feature.
2. Select StaticFileModule in the list.
3. Click Remove in the Actions sidebar.
WARNING
If the IIS static file handler is enabled and the ASP.NET Core Module is configured incorrectly, static files are served. This
happens, for example, if the web.config file isn't deployed.
Place code files (including .cs and .cshtml) outside of the app project's web root. A logical separation is
therefore created between the app's client-side content and server-based code. This prevents server-side
code from being leaked.
Additional resources
Middleware
Introduction to ASP.NET Core
Routing in ASP.NET Core
8/22/2018 • 20 minutes to read • Edit Online
IMPORTANT
This document covers low-level ASP.NET Core routing. For information on ASP.NET Core MVC routing, see Routing to
controller actions in ASP.NET Core.
Routing basics
Routing uses routes (implementations of IRouter) to:
Map incoming requests to route handlers.
Generate URLs used in responses.
Generally, an app has a single collection of routes. When a request arrives, the route collection is processed in
order. The incoming request looks for a route that matches the request URL by calling the RouteAsync method
on each available route in the route collection. By contrast, a response can use routing to generate URLs (for
example, for redirection or links) based on route information and thus avoid having to hard-code URLs, which
helps maintainability.
Routing is connected to the middleware pipeline by the RouterMiddleware class. ASP.NET Core MVC adds
routing to the middleware pipeline as part of its configuration. To learn about using routing as a standalone
component, see Use Routing Middleware section.
URL matching
URL matching is the process by which routing dispatches an incoming request to a handler. This process is
generally based on data in the URL path but can be extended to consider any data in the request. The ability to
dispatch requests to separate handlers is key to scaling the size and complexity of an app.
Incoming requests enter the RouterMiddleware , which calls the RouteAsync method on each route in sequence.
The IRouter instance chooses whether to handle the request by setting the RouteContext.Handler to a non-null
RequestDelegate. If a route sets a handler for the request, route processing stops, and the handler is invoked to
process the request. If all routes are tried and no handler is found for the request, the middleware calls next,
and the next middleware in the request pipeline is invoked.
The primary input to RouteAsyncis the RouteContext.HttpContext associated with the current request. The
RouteContext.Handler and RouteContext.RouteData are outputs set after a route matches.
A match during RouteAsync also sets the properties of the RouteContext.RouteData to appropriate values based
on the request processing performed thus far. If a route matches a request, the RouteContext.RouteData
contains important state information about the result.
RouteData.Values is a dictionary of route values produced from the route. These values are usually determined
by tokenizing the URL and can be used to accept user input or to make further dispatching decisions inside the
app.
RouteData.DataTokens is a property bag of additional data related to the matched route. DataTokens are
provided to support associating state data with each route so that the app can make decisions later based on
which route matched. These values are developer-defined and do not affect the behavior of routing in any way.
Additionally, values stashed in data tokens can be of any type, in contrast to route values, which must be easily
convertible to and from strings.
RouteData.Routers is a list of the routes that took part in successfully matching the request. Routes can be
nested inside of one another. The Routers property reflects the path through the logical tree of routes that
resulted in a match. Generally, the first item in Routers is the route collection and should be used for URL
generation. The last item in Routers is the route handler that matched.
URL generation
URL generation is the process by which routing can create a URL path based on a set of route values. This
allows for a logical separation between your handlers and the URLs that access them.
URL generation follows a similar iterative process, but it starts with user or framework code calling into the
GetVirtualPath method of the route collection. Each route then has its GetVirtualPath method called in
sequence until a non-null VirtualPathData is returned.
The primary inputs to GetVirtualPath are:
VirtualPathContext.HttpContext
VirtualPathContext.Values
VirtualPathContext.AmbientValues
Routes primarily use the route values provided by the Values and AmbientValues to decide where it's possible
to generate a URL and what values to include. The AmbientValues are the set of route values that were
produced from matching the current request with the routing system. In contrast, Values are the route values
that specify how to generate the desired URL for the current operation. The HttpContext is provided in case a
route needs to obtain services or additional data associated with the current context.
TIP
Think of VirtualPathContext.Values as being a set of overrides for the VirtualPathContext.AmbientValues. URL generation
attempts to reuse route values from the current request to make it easy to generate URLs for links using the same route
or route values.
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
This template matches a URL path like /Products/Details/17 and extracts the route values
{ controller = Products, action = Details, id = 17 } . The route values are determined by splitting the URL
path into segments and matching each segment with the route parameter name in the route template. Route
parameters are named. They're defined by enclosing the parameter name in braces { ... } .
The preceding template could also match the URL path / and would produce the values
{ controller = Home, action = Index } . This occurs because the {controller} and {action} route parameters
have default values and the id route parameter is optional. An equals = sign followed by a value after the
route parameter name defines a default value for the parameter. A question mark ? after the route parameter
name defines the parameter as optional. Route parameters with a default value always produce a route value
when the route matches. Optional parameters don't produce a route value if there was no corresponding URL
path segment.
See route-template-reference for a thorough description of route template features and syntax.
This example includes a route constraint:
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id:int}");
This template matches a URL path like /Products/Details/17 but not /Products/Details/Apples . The route
parameter definition {id:int} defines a route constraint for the id route parameter. Route constraints
implement IRouteConstraint and inspect route values to verify them. In this example, the route value id must
be convertible to an integer. See route-constraint-reference for a more detailed explanation of route constraints
that are provided by the framework.
Additional overloads of MapRoute accept values for constraints , dataTokens , and defaults . These additional
parameters of MapRoute are defined as type object . The typical usage of these parameters is to pass an
anonymously-typed object, where the property names of the anonymous type match route parameter names.
The following two examples create equivalent routes:
routes.MapRoute(
name: "default_route",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "default_route",
template: "{controller=Home}/{action=Index}/{id?}");
TIP
The inline syntax for defining constraints and defaults can be convenient for simple routes. However, there are features,
such as data tokens, that aren't supported by inline syntax.
routes.MapRoute(
name: "blog",
template: "Blog/{*article}",
defaults: new { controller = "Blog", action = "ReadArticle" });
This template matches a URL path like and extracts the values
/Blog/All-About-Routing/Introduction
{ controller = Blog, action = ReadArticle, article = All-About-Routing/Introduction } . The default route
values for controller and action are produced by the route even though there are no corresponding route
parameters in the template. Default values can be specified in the route template. The article route
parameter is defined as a catch-all by the appearance of an asterisk * before the route parameter name.
Catch-all route parameters capture the remainder of the URL path and can also match the empty string.
This example adds route constraints and data tokens:
routes.MapRoute(
name: "us_english_products",
template: "en-US/Products/{id}",
defaults: new { controller = "Products", action = "Details" },
constraints: new { id = new IntRouteConstraint() },
dataTokens: new { locale = "en-US" });
This template matches a URL path like /en-US/Products/5 and extracts the values
{ controller = Products, action = Details, id = 5 } and the data tokens { locale = en-US } .
URL generation
The Route class can also perform URL generation by combining a set of route values with its route template.
This is logically the reverse process of matching the URL path.
TIP
To better understand URL generation, imagine what URL you want to generate and then think about how a route
template would match that URL. What values would be produced? This is the rough equivalent of how URL generation
works in the Route class.
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
With the route values { controller = Products, action = List } , this route generates the URL /Products/List .
The route values are substituted for the corresponding route parameters to form the URL path. Since id is an
optional route parameter, it's no problem that it doesn't have a value.
With the route values { controller = Home, action = Index } , this route generates the URL / . The route
values that were provided match the default values so that the segments corresponding to those values can be
safely omitted. Note that both URLs generated round-trip with this route definition and produce the same
route values that were used to generate the URL.
TIP
An app using ASP.NET Core MVC should use UrlHelper to generate URLs instead of calling into routing directly.
For more details about the URL generation process, see url-generation-reference.
Routes must be configured in the Startup.Configure method. The sample app uses these APIs:
RouteBuilder
Build
MapGet – Matches only HTTP GET requests.
UseRouter
routeBuilder.MapRoute(
"Track Package Route",
"package/{operation:regex(^track|create|detonate$)}/{id:int}");
routeBuilder.MapRoute(
"Track Package Route",
"package/{operation:regex(^track|create|detonate$)}/{id:int}");
The following table shows the responses with the given URIs.
URI RESPONSE
If you're configuring a single route, call UseRouter passing in an IRouter instance. You won't need to use
RouteBuilder.
The framework provides a set of extension methods for creating routes, such as:
MapRoute
MapGet
MapPost
MapPut
MapDelete
MapVerb
Some of these methods, such as MapGet , require a RequestDelegate to be provided. The RequestDelegate is
used as the route handler when the route matches. Other methods in this family allow configuring a
middleware pipeline for use as the route handler. If the Map method doesn't accept a handler, such as
MapRoute , then it uses the DefaultHandler.
The Map[Verb] methods use constraints to limit the route to the HTTP Verb in the method name. For example,
see MapGet and MapVerb.
You can use the * character as a prefix to a route parameter to bind to the rest of the URI. This is called a
catch-all parameter. For example, blog/{*slug} matches any URI that starts with /blog and had any value
following it (which is assigned to the slug route value). Catch-all parameters can also match the empty string.
Route parameters may have default values, designated by specifying the default after the parameter name,
separated by an equals sign ( = ). For example, {controller=Home} defines Home as the default value for
controller . The default value is used if no value is present in the URL for the parameter. In addition to default
values, route parameters may be optional, specified by appending a question mark ( ? ) to the end of the
parameter name, as in id? . The difference between optional values and default route parameters is that a
route parameter with a default value always produces a value; an optional parameter has a value only when a
value is provided by the request URL.
Route parameters may also have constraints, which must match the route value bound from the URL. Adding a
colon : and constraint name after the route parameter name specifies an inline constraint on a route
parameter. If the constraint requires arguments, they're provided in enclosed in parentheses ( ) after the
constraint name. Multiple inline constraints can be specified by appending another colon : and constraint
name. The constraint name is passed to the IInlineConstraintResolver service to create an instance of
IRouteConstraint to use in URL processing. For example, the route template blog/{article:minlength(10)}
specifies a minlength constraint with the argument 10 . For more information on route constraints and a
listing of the constraints provided by the framework, see the Route constraint reference section.
The following table demonstrates some route templates and their behavior.
Using a template is generally the simplest approach to routing. Constraints and defaults can also be specified
outside the route template.
TIP
Enable Logging to see how the built in routing implementations, such as Route , match requests.
WARNING
Avoid using constraints for input validation because doing so means that invalid input results in a 404 - Not Found
response instead of a 400 - Bad Request with an appropriate error message. Route constraints are used to
disambiguate between similar routes, not to validate the inputs for a particular route.
The following table demonstrates some route constraints and their expected behavior.
Multiple, colon-delimited constraints can be applied to a single parameter. For example, the following
constraint restricts a parameter to an integer value of 1 or greater:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
WARNING
Route constraints that verify the URL and are converted to a CLR type (such as int or DateTime ) always use the
invariant culture. These constraints assume that the URL is non-localizable. The framework-provided route constraints
don't modify the values stored in route values. All route values parsed from the URL are stored as strings. For example,
the float constraint attempts to convert the route value to a float, but the converted value is used only to verify it
can be converted to a float.
Regular expressions
The ASP.NET Core framework adds
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant to the regular expression
constructor. See RegexOptions for a description of these members.
Regular expressions use delimiters and tokens similar to those used by Routing and the C# language. Regular
expression tokens must be escaped. To use the regular expression ^\d{3}-\d{2}-\d{4}$ in Routing, the
expression must have the \ characters typed in as \\ in the C# source file to escape the \ string escape
character (unless using verbatim string literals. The { , } , [ and ] characters must be escaped by
doubling them to escape the Routing parameter delimiter characters. The table below shows a regular
expression and the escaped version.
EXPRESSION ESCAPED
^\d{3}-\d{2}-\d{4}$ ^\\d{{3}}-\\d{{2}}-\\d{{4}}$
^[a-z]{2}$ ^[[a-z]]{{2}}$
Regular expressions used in routing often start with the ^ character (match starting position of the string) and
end with the $ character (match ending position of the string). The ^ and $ characters ensure that the
regular expression match the entire route parameter value. Without the ^ and $ characters, the regular
expression match any substring within the string, which is often undesirable. The table below shows some
examples and explains why they match or fail to match.
Refer to .NET Framework Regular Expressions for more information on regular expression syntax.
To constrain a parameter to a known set of possible values, use a regular expression. For example,
{action:regex(^(list|get|create)$)} only matches the action route value to list , get , or create . If
passed into the constraints dictionary, the string ^(list|get|create)$ is equivalent. Constraints that are passed
in the constraints dictionary (not inline within a template) that don't match one of the known constraints are
also treated as regular expressions.
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("Menu<hr/>");
await context.Response.WriteAsync(
$"<a href='{path}'>Create Package 123</a><br/>");
});
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("Menu<hr/>");
await context.Response.WriteAsync(
$"<a href='{path}'>Create Package 123</a><br/>");
});
The VirtualPath generated at the end of the preceding sample is /package/create/123 . The dictionary supplies
the operation and id route values of the "Track Package Route" template, package/{operation}/{id} . For
details, see the sample code in the Use Routing Middleware section or the sample app.
The second parameter to the VirtualPathContext constructor is a collection of ambient values. Ambient values
provide convenience by limiting the number of values a developer must specify within a certain request
context. The current route values of the current request are considered ambient values for link generation. In an
ASP.NET Core MVC app if you are in the About action of the HomeController , you don't need to specify the
controller route value to link to the Index action—the ambient value of Home is used.
Ambient values that don't match a parameter are ignored, and ambient values are also ignored when an
explicitly-provided value overrides it, going from left to right in the URL.
Values that are explicitly provided but which don't match anything are added to the query string. The following
table shows the result when using the route template {controller}/{action}/{id?} .
If a route has a default value that doesn't correspond to a parameter and that value is explicitly provided, it
must match the default value:
routes.MapRoute("blog_route", "blog/{*slug}",
defaults: new { controller = "Blog", action = "ReadPost" });
Link generation only generates a link for this route when the matching values for controller and action are
provided.
URL Rewriting Middleware in ASP.NET Core
6/21/2018 • 16 minutes to read • Edit Online
NOTE
URL rewriting can reduce the performance of an app. Where feasible, you should limit the number and complexity of rules.
Package
To include the middleware in your project, add a reference to the Microsoft.AspNetCore.Rewrite package. This
feature is available for apps that target ASP.NET Core 1.1 or later.
app.UseRewriter(options);
}
URL redirect
Use AddRedirect to redirect requests. The first parameter contains your regex for matching on the path of the
incoming URL. The second parameter is the replacement string. The third parameter, if present, specifies the
status code. If you don't specify the status code, it defaults to 302 (Found), which indicates that the resource is
temporarily moved or replaced.
ASP.NET Core 2.x
ASP.NET Core 1.x
public void Configure(IApplicationBuilder app)
{
using (StreamReader apacheModRewriteStreamReader =
File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
File.OpenText("IISUrlRewrite.xml"))
{
var options = new RewriteOptions()
.AddRedirect("redirect-rule/(.*)", "redirected/$1")
.AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
skipRemainingRules: true)
.AddApacheModRewrite(apacheModRewriteStreamReader)
.AddIISUrlRewrite(iisUrlRewriteStreamReader)
.Add(MethodRules.RedirectXMLRequests)
.Add(new RedirectImageRequests(".png", "/png-images"))
.Add(new RedirectImageRequests(".jpg", "/jpg-images"));
app.UseRewriter(options);
}
In a browser with developer tools enabled, make a request to the sample app with the path
/redirect-rule/1234/5678 . The regex matches the request path on redirect-rule/(.*) , and the path is replaced
with /redirected/1234/5678 . The redirect URL is sent back to the client with a 302 (Found) status code. The
browser makes a new request at the redirect URL, which appears in the browser's address bar. Since no rules in
the sample app match on the redirect URL, the second request receives a 200 (OK) response from the app and
the body of the response shows the redirect URL. A roundtrip is made to the server when a URL is redirected.
WARNING
Be cautious when establishing your redirect rules. Your redirect rules are evaluated on each request to the app, including
after a redirect. It's easy to accidently create a loop of infinite redirects.
The part of the expression contained within parentheses is called a capture group. The dot ( . ) of the expression
means match any character. The asterisk ( * ) indicates match the preceding character zero or more times.
Therefore, the last two path segments of the URL, 1234/5678 , are captured by capture group (.*) . Any value
you provide in the request URL after redirect-rule/ is captured by this single capture group.
In the replacement string, captured groups are injected into the string with the dollar sign ( $ ) followed by the
sequence number of the capture. The first capture group value is obtained with $1 , the second with $2 , and
they continue in sequence for the capture groups in your regex. There's only one captured group in the redirect
rule regex in the sample app, so there's only one injected group in the replacement string, which is $1 . When the
rule is applied, the URL becomes /redirected/1234/5678 .
URL redirect to a secure endpoint
Use AddRedirectToHttps to redirect HTTP requests to the same host and path using HTTPS ( https:// ). If the
status code isn't supplied, the middleware defaults to 302 (Found). If the port isn't supplied, the middleware
defaults to null , which means the protocol changes to https:// and the client accesses the resource on port
443. The example shows how to set the status code to 301 (Moved Permanently) and change the port to 5001.
app.UseRewriter(options);
}
Use AddRedirectToHttpsPermanent to redirect insecure requests to the same host and path with secure HTTPS
protocol ( https:// on port 443). The middleware sets the status code to 301 (Moved Permanently).
app.UseRewriter(options);
}
NOTE
When redirecting to HTTPS without the requirement for additional redirect rules, we recommend using HTTPS Redirection
Middleware. For more information, see the Enforce HTTPS topic.
app.UseRewriter(options);
}
PATH MATCH
/redirect-rule/1234/5678 Yes
/my-cool-redirect-rule/1234/5678 Yes
/anotherredirect-rule/1234/5678 Yes
The rewrite rule, ^rewrite-rule/(\d+)/(\d+) , only matches paths if they start with rewrite-rule/ . Notice the
difference in matching between the rewrite rule below and the redirect rule above.
PATH MATCH
/rewrite-rule/1234/5678 Yes
/my-cool-rewrite-rule/1234/5678 No
/anotherrewrite-rule/1234/5678 No
Following the ^rewrite-rule/ portion of the expression, there are two capture groups, (\d+)/(\d+) . The \d
signifies match a digit (number ). The plus sign ( + ) means match one or more of the preceding character.
Therefore, the URL must contain a number followed by a forward-slash followed by another number. These
capture groups are injected into the rewritten URL as $1 and $2 . The rewrite rule replacement string places the
captured groups into the querystring. The requested path of /rewrite-rule/1234/5678 is rewritten to obtain the
resource at /rewritten?var1=1234&var2=5678 . If a querystring is present on the original request, it's preserved
when the URL is rewritten.
There's no roundtrip to the server to obtain the resource. If the resource exists, it's fetched and returned to the
client with a 200 (OK) status code. Because the client isn't redirected, the URL in the browser address bar doesn't
change. As far as the client is concerned, the URL rewrite operation never occurred.
NOTE
Use skipRemainingRules: true whenever possible, because matching rules is an expensive process and reduces app
response time. For the fastest app response:
Order your rewrite rules from the most frequently matched rule to the least frequently matched rule.
Skip the processing of the remaining rules when a match occurs and no additional rule processing is required.
Apache mod_rewrite
Apply Apache mod_rewrite rules with AddApacheModRewrite . Make sure that the rules file is deployed with the app.
For more information and examples of mod_rewrite rules, see Apache mod_rewrite.
ASP.NET Core 2.x
ASP.NET Core 1.x
A StreamReader is used to read the rules from the ApacheModRewrite.txt rules file.
app.UseRewriter(options);
}
The sample app redirects requests from /apache-mod-rules-redirect/(.\*) to /redirected?id=$1 . The response
status code is 302 (Found).
app.UseRewriter(options);
}
The sample app rewrites requests from /iis-rules-rewrite/(.*) to /rewritten?id=$1 . The response is sent to the
client with a 200 (OK) status code.
<rewrite>
<rules>
<rule name="Rewrite segment to id querystring" stopProcessing="true">
<match url="^iis-rules-rewrite/(.*)$" />
<action type="Rewrite" url="rewritten?id={R:1}" appendQueryString="false"/>
</rule>
</rules>
</rewrite>
If you have an active IIS Rewrite Module with server-level rules configured that would impact your app in
undesirable ways, you can disable the IIS Rewrite Module for an app. For more information, see Disabling IIS
modules.
Unsupported features
ASP.NET Core 2.x
ASP.NET Core 1.x
The middleware released with ASP.NET Core 2.x doesn't support the following IIS URL Rewrite Module features:
Outbound Rules
Custom Server Variables
Wildcards
LogRewrittenUrl
Supported server variables
The middleware supports the following IIS URL Rewrite Module server variables:
CONTENT_LENGTH
CONTENT_TYPE
HTTP_ACCEPT
HTTP_CONNECTION
HTTP_COOKIE
HTTP_HOST
HTTP_REFERER
HTTP_URL
HTTP_USER_AGENT
HTTPS
LOCAL_ADDR
QUERY_STRING
REMOTE_ADDR
REMOTE_PORT
REQUEST_FILENAME
REQUEST_URI
NOTE
You can also obtain an IFileProvider via a PhysicalFileProvider . This approach may provide greater flexibility for the
location of your rewrite rules files. Make sure that your rewrite rules files are deployed to the server at the path you
provide.
Method-based rule
Use Add(Action<RewriteContext> applyRule) to implement your own rule logic in a method. The RewriteContext
exposes the HttpContext for use in your method. The context.Result determines how additional pipeline
processing is handled.
RuleResult.SkipRemainingRules Stop applying rules and send the context to the next
middleware
app.UseRewriter(options);
}
The sample app demonstrates a method that redirects requests for paths that end with .xml. If you make a request
for /file.xml , it's redirected to /xmlfiles/file.xml . The status code is set to 301 (Moved Permanently). For a
redirect, you must explicitly set the status code of the response; otherwise, a 200 (OK) status code is returned and
the redirect won't occur on the client.
if (request.Path.Value.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] =
"/xmlfiles" + request.Path + request.QueryString;
}
}
Original Request: /file.xml
app.UseRewriter(options);
}
The values of the parameters in the sample app for the extension and the newPath are checked to meet several
conditions. The extension must contain a value, and the value must be .png, .jpg, or .gif. If the newPath isn't valid,
an ArgumentException is thrown. If you make a request for image.png, it's redirected to /png-images/image.png . If
you make a request for image.jpg, it's redirected to /jpg-images/image.jpg . The status code is set to 301 (Moved
Permanently), and the context.Result is set to stop processing rules and send the response.
public class RedirectImageRequests : IRule
{
private readonly string _extension;
private readonly PathString _newPath;
if (!Regex.IsMatch(extension, @"^\.(png|jpg|gif)$"))
{
throw new ArgumentException("Invalid extension", nameof(extension));
}
if (!Regex.IsMatch(newPath, @"(/[A-Za-z0-9]+)+?"))
{
throw new ArgumentException("Invalid path", nameof(newPath));
}
_extension = extension;
_newPath = new PathString(newPath);
}
if (request.Path.Value.EndsWith(_extension, StringComparison.OrdinalIgnoreCase))
{
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] =
_newPath + request.Path + request.QueryString;
}
}
}
Additional resources
Application Startup
Middleware
Regular expressions in .NET
Regular expression language - quick reference
Apache mod_rewrite
Using Url Rewrite Module 2.0 (for IIS )
URL Rewrite Module Configuration Reference
IIS URL Rewrite Module Forum
Keep a simple URL structure
10 URL Rewriting Tips and Tricks
To slash or not to slash
Use multiple environments in ASP.NET Core
8/25/2018 • 8 minutes to read • Edit Online
By Rick Anderson
ASP.NET Core configures app behavior based on the runtime environment using an environment variable.
View or download sample code (how to download)
Environments
ASP.NET Core reads the environment variable ASPNETCORE_ENVIRONMENT at app startup and stores the value in
IHostingEnvironment.EnvironmentName. You can set ASPNETCORE_ENVIRONMENT to any value, but three values are
supported by the framework: Development, Staging, and Production. If ASPNETCORE_ENVIRONMENT isn't set, it
defaults to Production .
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
Calls UseExceptionHandler when the value of ASPNETCORE_ENVIRONMENT is set one of the following:
Staging
Production
Staging_2
The Environment Tag Helper uses the value of IHostingEnvironment.EnvironmentName to include or exclude
markup in the element:
@page
@inject Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnv
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>
On Windows and macOS, environment variables and values aren't case sensitive. Linux environment variables
and values are case sensitive by default.
Development
The development environment can enable features that shouldn't be exposed in production. For example, the
ASP.NET Core templates enable the developer exception page in the development environment.
The environment for local machine development can be set in the Properties\launchSettings.json file of the
project. Environment values set in launchSettings.json override values set in the system environment.
The following JSON shows three profiles from a launchSettings.json file:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54339/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_My_Environment": "1",
"ASPNETCORE_DETAILEDERRORS": "1",
"ASPNETCORE_ENVIRONMENT": "Staging"
}
},
"EnvironmentsSample": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Staging"
},
"applicationUrl": "http://localhost:54340/"
},
"Kestrel Staging": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_My_Environment": "1",
"ASPNETCORE_DETAILEDERRORS": "1",
"ASPNETCORE_ENVIRONMENT": "Staging"
},
"applicationUrl": "http://localhost:51997/"
}
}
}
NOTE
The applicationUrl property in launchSettings.json can specify a list of server URLs. Use a semicolon between the
URLs in the list:
"EnvironmentsSample": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
When the app is launched with dotnet run, the first profile with "commandName": "Project" is used. The value of
commandName specifies the web server to launch. commandName can be any one of the following:
IIS Express
IIS
Project (which launches Kestrel)
When an app is launched with dotnet run:
launchSettings.json is read if available. environmentVariables settings in launchSettings.json override
environment variables.
The hosting environment is displayed.
The following output shows an app started with dotnet run:
The Visual Studio project properties Debug tab provides a GUI to edit the launchSettings.json file:
Changes made to project profiles may not take effect until the web server is restarted. Kestrel must be restarted
before it can detect changes made to its environment.
WARNING
launchSettings.json shouldn't store secrets. The Secret Manager tool can be used to store secrets for local development.
When using Visual Studio Code, environment variables can be set in the .vscode/launch.json file. The following
example sets the environment to Development :
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
]
}
A .vscode/launch.json file in the project isn't read when starting the app with dotnet run in the same way as
Properties/launchSettings.json. When launching an app in development that doesn't have a launchSettings.json
file, either set the environment with an environment variable or a command-line argument to the dotnet run
command.
Production
The production environment should be configured to maximize security, performance, and app robustness.
Some common settings that differ from development include:
Caching.
Client-side resources are bundled, minified, and potentially served from a CDN.
Diagnostic error pages disabled.
Friendly error pages enabled.
Production logging and monitoring enabled. For example, Application Insights.
set ASPNETCORE_ENVIRONMENT=Development
PowerShell
$Env:ASPNETCORE_ENVIRONMENT = "Development"
These commands only take effect for the current window. When the window is closed, the
ASPNETCORE_ENVIRONMENT setting reverts to the default setting or machine value.
To set the value globally in Windows, use either of the following approaches:
Open the Control Panel > System > Advanced system settings and add or edit the
ASPNETCORE_ENVIRONMENT value:
Open an administrative command prompt and use the setx command or open an administrative
PowerShell command prompt and use [Environment]::SetEnvironmentVariable :
Command prompt
The /M switch indicates to set the environment variable at the system level. If the /M switch isn't used,
the environment variable is set for the user account.
PowerShell
The Machine option value indicates to set the environment variable at the system level. If the option value
is changed to User , the environment variable is set for the user account.
When the ASPNETCORE_ENVIRONMENT environment variable is set globally, it takes effect for dotnet run in any
command window opened after the value is set.
web.config
To set the ASPNETCORE_ENVIRONMENT environment variable with web.config, see the Setting environment variables
section of ASP.NET Core Module configuration reference. When the ASPNETCORE_ENVIRONMENT environment
variable is set with web.config, its value overrides a setting at the system level.
Per IIS Application Pool
To set the ASPNETCORE_ENVIRONMENT environment variable for an app running in an isolated Application Pool
(supported on IIS 10.0 or later), see the AppCmd.exe command section of the Environment Variables
<environmentVariables> topic. When the ASPNETCORE_ENVIRONMENT environment variable is set for an app pool,
its value overrides a setting at the system level.
IMPORTANT
When hosting an app in IIS and adding or changing the ASPNETCORE_ENVIRONMENT environment variable, use any one of
the following approaches to have the new value picked up by apps:
Restart an app's app pool.
Execute net stop was /y followed by net start w3svc from a command prompt.
Restart the server.
macOS
Setting the current environment for macOS can be performed in-line when running the app:
Alternatively, set the environment with export prior to running the app:
export ASPNETCORE_ENVIRONMENT=Development
Machine-level environment variables are set in the .bashrc or .bash_profile file. Edit the file using any text editor.
Add the following statement:
export ASPNETCORE_ENVIRONMENT=Development
Linux
For Linux distros, use the export command at a command prompt for session-based variable settings and
bash_profile file for machine-level environment settings.
Configuration by environment
To load configuration by environment, we recommend:
appsettings files (*appsettings.<>.json). See Configuration: File configuration provider.
environment variables (set on each system where the app is hosted). See Configuration: File configuration
provider and Safe storage of app secrets in development: Environment variables.
Secret Manager (in the Development environment only). See Safe storage of app secrets in development in
ASP.NET Core.
return WebHost.CreateDefaultBuilder(args)
.UseStartup(assemblyName);
}
public static void Main(string[] args)
{
CreateWebHost(args).Run();
}
return WebHost.CreateDefaultBuilder(args)
.UseStartup(assemblyName)
.Build();
}
host.Run();
}
}
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
app.UseExceptionHandler("/Error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
}
Additional resources
Application startup in ASP.NET Core
Configuration in ASP.NET Core
IHostingEnvironment.EnvironmentName
Configuration in ASP.NET Core
9/6/2018 • 31 minutes to read • Edit Online
By Luke Latham
App configuration in ASP.NET Core is based on key-value pairs established by configuration providers.
Configuration providers read configuration data into key-value pairs from a variety of configuration sources:
Azure Key Vault
Command-line arguments
Custom providers (installed or created)
Directory files
Environment variables
In-memory .NET objects
Settings files
Azure Key Vault
Command-line arguments
Custom providers (installed or created)
Environment variables
In-memory .NET objects
Settings files
Command-line arguments
Custom providers (installed or created)
Environment variables
In-memory .NET objects
Settings files
The options pattern is an extension of the configuration concepts described in this topic. Options uses classes to
represent groups of related settings. For more information on using the options pattern, see Options pattern in
ASP.NET Core.
View or download sample code (how to download)
The examples provided in this topic rely upon:
Setting the base path of the app with SetBasePath. SetBasePath is made available to an app by referencing the
Microsoft.Extensions.Configuration.FileExtensions package.
Resolving sections of configuration files with GetSection. GetSection is made available to an app by referencing
the Microsoft.Extensions.Configuration package.
Binding configuration to .NET classes with Bind and Get<T>. Bind and Get<T> are made available to an app
by referencing the Microsoft.Extensions.Configuration.Binder package. Get<T> is available in ASP.NET Core 1.1
or later.
These three packages are included in the Microsoft.AspNetCore.App metapackage.
These three packages are included in the Microsoft.AspNetCore.All metapackage.
Security
Adopt the following best practices:
Never store passwords or other sensitive data in configuration provider code or in plain text configuration files.
Don't use production secrets in development or test environments.
Specify secrets outside of the project so that they can't be accidentally committed to a source code repository.
Learn more about how to use multiple environments and managing the safe storage of app secrets in development
with the Secret Manager (includes advice on using environment variables to store sensitive data). The Secret
Manager uses the File Configuration Provider to store user secrets in a JSON file on the local system. The File
Configuration Provider is described later in this topic.
Azure Key Vault is one option for the safe storage of app secrets. For more information, see Azure Key Vault
configuration provider in ASP.NET Core.
{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
}
}
When the file is read into configuration, unique keys are created to maintain the original hierarchical data structure
of the configuration source. The sections and keys are flattened with the use of a colon ( : ) to maintain the original
structure:
section0:key0
section0:key1
section1:key0
section1:key1
GetSection and GetChildren methods are available to isolate sections and children of a section in the configuration
data. These methods are described later in GetSection, GetChildren, and Exists.
Conventions
At app startup, configuration sources are read in the order that their configuration providers are specified.
File Configuration Providers have the ability to reload configuration when an underlying settings file is changed
after app startup. The File Configuration Provider is described later in this topic.
IConfiguration is available in the app's Dependency Injection (DI) container. Configuration providers can't utilize DI,
as it's not available when they're set up by the host.
Configuration keys adopt the following conventions:
Keys are case-insensitive. For example, ConnectionString and connectionstring are treated as equivalent keys.
If a value for the same key is set by the same or different configuration providers, the last value set on the key is
the value used.
Hierarchical keys
Within the Configuration API, a colon separator ( : ) works on all platforms.
In environment variables, a colon separator may not work on all platforms. A double underscore ( __ ) is
supported by all platforms and is converted to a colon.
In Azure Key Vault, hierarchical keys use -- (two dashes) as a separator. You must provide code to
replace the dashes with a colon when the secrets are loaded into the app's configuration.
The ConfigurationBinder supports binding arrays to objects using array indices in configuration keys. Array
binding is described in the Bind an array to a class section.
Configuration values adopt the following conventions:
Values are strings.
Null values can't be stored in configuration or bound to objects.
Providers
The following table shows the configuration providers available to ASP.NET Core apps.
Azure Key Vault Configuration Provider (Security topics) Azure Key Vault
User secrets (Secret Manager) (Security topics) File in the user profile directory
Azure Key Vault Configuration Provider (Security topics) Azure Key Vault
User secrets (Secret Manager) (Security topics) File in the user profile directory
User secrets (Secret Manager) (Security topics) File in the user profile directory
Configuration sources are read in the order that their configuration providers are specified at startup. The
configuration providers described in this topic are described in alphabetical order, not in the order that your code
may arrange them. Order configuration providers in your code to suit your priorities for the underlying
configuration sources.
A typical sequence of configuration providers is:
1. Files (appsettings.json, appsettings.<Environment>.json, where <Environment> is the app's current hosting
environment)
2. User secrets (Secret Manager) (in the Development environment only)
3. Environment variables
4. Command-line arguments
It's a common practice to position the Command-line Configuration Provider last in a series of providers to allow
command-line arguments to override configuration set by the other providers.
This sequence of providers is put into place when you initialize a new WebHostBuilder with CreateDefaultBuilder.
For more information, see Web Host: Set up a host.
Call ConfigureAppConfiguration when building the Web Host to specify the app's configuration providers:
public class Program
{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};
if (appAssembly != null)
{
builder.AddUserSecrets(appAssembly, optional: true);
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
In the preceding example, the environment name ( env.EnvironmentName ) and app assembly name (
env.ApplicationName) are provided by the IHostingEnvironment. For more information, see Use multiple
environments in ASP.NET Core.
Example
The 2.x sample app takes advantage of the static convenience method CreateDefaultBuilder to build the host,
which includes a call to AddCommandLine.
The 1.x sample app calls AddCommandLine on a ConfigurationBuilder.
1. Open a command prompt in the project's directory.
2. Supply a command-line argument to the dotnet run command, dotnet run CommandLineKey=CommandLineValue .
3. After the app is running, open a browser to the app at http://localhost:5000 .
4. Observe that the output contains the key-value pair for the configuration command-line argument provided to
dotnet run .
Arguments
The value must follow an equals sign ( = ), or the key must have a prefix ( -- or / ) when the value follows a space.
The value can be null if an equals sign is used (for example, CommandLineKey= ).
No prefix CommandLineKey1=value1
Within the same command, don't mix command-line argument key-value pairs that use an equals sign with key-
value pairs that use a space.
Example commands:
Switch mappings
Switch mappings allow key name replacement logic. When you manually build configuration with a
ConfigurationBuilder, you can provide a dictionary of switch replacements to the AddCommandLine method.
When the switch mappings dictionary is used, the dictionary is checked for a key that matches the key provided by
a command-line argument. If the command-line key is found in the dictionary, the dictionary value (the key
replacement) is passed back to set the key-value pair into the app's configuration. A switch mapping is required for
any command-line key prefixed with a single dash ( - ).
Switch mappings dictionary key rules:
Switches must start with a dash ( - ) or double-dash ( -- ).
The switch mappings dictionary must not contain duplicate keys.
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
return WebHost.CreateDefaultBuilder()
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
As shown in the preceding example, the call to CreateDefaultBuilder shouldn't pass arguments when switch
mappings are used. CreateDefaultBuilder method's AddCommandLine call doesn't include mapped switches, and
there's no way to pass the switch mapping dictionary to CreateDefaultBuilder . If the arguments include a mapped
switch and are passed to CreateDefaultBuilder , its AddCommandLine provider fails to initialize with a
FormatException. The solution is not to pass the arguments to CreateDefaultBuilder but instead to allow the
ConfigurationBuilder method's AddCommandLine method to process both the arguments and the switch mapping
dictionary.
using (host)
{
Console.ReadLine();
}
}
After the switch mappings dictionary is created, it contains the data shown in the following table.
KEY VALUE
-CLKey1 CommandLineKey1
-CLKey2 CommandLineKey2
If the switch-mapped keys are used when starting the app, configuration receives the configuration value on the
key supplied by the dictionary:
After running the preceding command, configuration contains the values shown in the following table.
KEY VALUE
CommandLineKey1 value1
CommandLineKey2 value2
Example
The 2.x sample app takes advantage of the static convenience method CreateDefaultBuilder to build the host,
which includes a call to AddEnvironmentVariables .
The 1.x sample app calls AddEnvironmentVariables on a ConfigurationBuilder .
1. Run the sample app. Open a browser to the app at http://localhost:5000 .
2. Observe that the output contains the key-value pair for the environment variable ENVIRONMENT . The value
reflects the environment in which the app is running, typically Development when running locally.
To keep the list of environment variables rendered by the app short, the app filters environment variables to those
that start with the following:
ASPNETCORE_
urls
Logging
ENVIRONMENT
contentRoot
AllowedHosts
applicationName
CommandLine
If you wish to expose all of the environment variables available to the app, change the FilteredConfiguration in
Pages/Index.cshtml.cs to the following:
If you wish to expose all of the environment variables available to the app, change the FilteredConfiguration in
Controllers/HomeController.cs to the following:
FilteredConfiguration = _config.AsEnumerable();
Prefixes
Environment variables loaded into the app's configuration are filtered when you supply a prefix to the
AddEnvironmentVariables method. For example, to filter environment variables on the prefix CUSTOM_ , supply the
prefix to the configuration provider:
The prefix is stripped off when the configuration key-value pairs are created.
The static convenience method CreateDefaultBuilder creates a WebHostBuilder to establish the app's host. When
WebHostBuilder is created, it finds its host configuration in environment variables prefixed with ASPNETCORE_ .
MYSQLCONNSTR_ MySQL
When an environment variable is discovered and loaded into configuration with any of the four prefixes shown in
the table:
The configuration key is created by removing the environment variable prefix and adding a configuration key
section ( ConnectionStrings ).
A new configuration key-value pair is created that represents the database connection provider (except for
CUSTOMCONNSTR_ , which has no stated provider ).
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
[section1]
subsection:key=value
[section2:subsection0]
key=value
[section2:subsection1]
key=value
The previous configuration file loads the following keys with value :
section0:key0
section0:key1
section1:subsection:key
section2:subsection0:key
section2:subsection1:key
JSON Configuration Provider
The JsonConfigurationProvider loads configuration from JSON file key-value pairs during runtime.
To activate JSON file configuration, call the AddJsonFile extension method on an instance of ConfigurationBuilder.
Overloads permit specifying:
Whether the file is optional.
Whether the configuration is reloaded if the file changes.
The IFileProvider used to access the file.
AddJsonFile is automatically called twice when you initialize a new WebHostBuilder with CreateDefaultBuilder.
The method is called to load configuration from:
appsettings.json – This file is read first. The environment version of the file can override the values provided by
the appsettings.json file.
appsettings.<Environment>.json – The environment version of the file is loaded based on the
IHostingEnvironment.EnvironmentName.
For more information, see Web Host: Set up a host.
CreateDefaultBuilder also loads:
Environment variables.
User secrets (Secret Manager) (in the Development environment).
Command-line arguments.
The JSON Configuration Provider is established first. Therefore, user secrets, environment variables, and
command-line arguments override configuration set by the appsettings files.
You can also directly call the AddJsonFile extension method on an instance of ConfigurationBuilder.
When calling CreateDefaultBuilder , call UseConfiguration with the configuration:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
Example
The 2.x sample app takes advantage of the static convenience method CreateDefaultBuilder to build the host,
which includes two calls to AddJsonFile . Configuration is loaded from appsettings.json and appsettings.
<Environment>.json.
The 1.x sample app calls AddJsonFile twice on a ConfigurationBuilder . Configuration is loaded from
appsettings.json and appsettings.<Environment>.json.
1. Run the sample app. Open a browser to the app at http://localhost:5000 .
2. Observe that the output contains key-value pairs for the configuration shown in the table depending on the
environment. Logging configuration keys use the colon ( : ) as a hierarchical separator.
AllowedHosts * *
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
XML configuration files can use distinct element names for repeating sections:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<section0>
<key0>value</key0>
<key1>value</key1>
</section0>
<section1>
<key0>value</key0>
<key1>value</key1>
</section1>
</configuration>
The previous configuration file loads the following keys with value :
section0:key0
section0:key1
section1:key0
section1:key1
Repeating elements that use the same element name work if the name attribute is used to distinguish the elements:
The previous configuration file loads the following keys with value :
section:section0:key:key0
section:section0:key:key1
section:section1:key:key0
section:section1:key:key1
Attributes can be used to supply values:
The previous configuration file loads the following keys with value :
key:attribute
section:key:attribute
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
GetValue
ConfigurationBinder.GetValue<T> extracts a value from configuration with a specified key and converts it to the
specified type. An overload permits you to provide a default value if the key isn't found.
The following example extracts the string value from configuration with the key NumberKey , types the value as an
int , and stores the value in the variable intValue . If NumberKey isn't found in the configuration keys, intValue
receives the default value of 99 :
When the file is read into configuration, the following unique hierarchical keys are created to hold the configuration
values:
section0:key0
section0:key1
section1:key0
section1:key1
section2:subsection0:key0
section2:subsection0:key1
section2:subsection1:key0
section2:subsection1:key1
GetSection
IConfiguration.GetSection extracts a configuration subsection with the specified subsection key.
To return an IConfigurationSection containing only the key-value pairs in section1 , call GetSection and supply the
section name:
Similarly, to obtain the values for keys in section2:subsection0 , call GetSection and supply the section path:
GetSection never returns null . If a matching section isn't found, an empty IConfigurationSection is returned.
GetChildren
A call to IConfiguration.GetChildren on section2 obtains an IEnumerable<IConfigurationSection> that includes:
subsection0
subsection1
var configSection = _config.GetSection("section2");
Exists
Use ConfigurationExtensions.Exists to determine if a configuration section exists:
Given the example data, sectionExists is false because there isn't a section2:subsection2 section in the
configuration data.
Bind to a class
Configuration can be bound to classes that represent groups of related settings using the options pattern. For more
information, see Options pattern in ASP.NET Core.
Configuration values are returned as strings, but calling Bind enables the construction of POCO objects.
The sample app contains a Starship model (Models/Starship.cs):
The starship section of the starship.json file creates the configuration when the sample app uses the JSON
Configuration Provider to load the configuration:
{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}
{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}
KEY VALUE
starship:registry NCC-1701
starship:class Constitution
starship:length 304.8
starship:commissioned False
The sample app calls GetSection with the starship key. The starship key-value pairs are isolated. The Bind
method is called on the subsection passing in an instance of the Starship class. After binding the instance values,
the instance is assigned to a property for rendering:
The sample app has a tvshow.xml file containing the configuration data:
Configuration is bound to the entire TvShow object graph with the Bind method. The bound instance is assigned
to a property for rendering:
ConfigurationBinder.Get<T> binds and returns the specified type. Get<T> is more convenient than using Bind .
The following code shows how to use Get<T> with the preceding example, which allows the bound instance to be
directly assigned to the property used for rendering:
TvShow = _config.GetSection("tvshow").Get<TvShow>();
viewModel.TvShow = _config.GetSection("tvshow").Get<TvShow>();
NOTE
Binding is provided by convention. Custom configuration providers aren't required to implement array binding.
KEY VALUE
array:0 value0
KEY VALUE
array:1 value1
array:2 value2
array:4 value4
array:5 value5
These keys and values are loaded in the sample app using the Memory Configuration Provider:
Configuration = builder.Build();
}
The array skips a value for index #3. The configuration binder isn't capable of binding null values or creating null
entries in bound objects, which becomes clear in a moment when the result of binding this array to an object is
demonstrated.
In the sample app, a POCO class is available to hold the bound configuration data:
ConfigurationBinder.Get<T> syntax can also be used, which results in more compact code:
ArrayExample = _config.GetSection("array").Get<ArrayExample>();
viewModel.ArrayExample = _config.GetSection("array").Get<ArrayExample>();
The bound object, an instance of ArrayExample , receives the array data from configuration.
ARRAYEXAMPLES.ENTRIES INDEX ARRAYEXAMPLES.ENTRIES VALUE
0 value0
1 value1
2 value2
3 value4
4 value5
Index #3 in the bound object holds the configuration data for the array:4 configuration key and its value of
value4 . When configuration data containing an array is bound, the array indices in the configuration keys are
merely used to iterate the configuration data when creating the object. A null value can't be retained in
configuration data, and a null-valued entry isn't created in a bound object when an array in configuration keys skip
one or more indices.
The missing configuration item for index #3 can be supplied before binding to the ArrayExamples instance by any
configuration provider that produces the correct key-value pair in configuration. If the sample included an
additional JSON Configuration Provider with the missing key-value pair, the ArrayExamples.Entries matches the
complete configuration array:
missing_value.json:
{
"array:entries:3": "value3"
}
In ConfigureAppConfiguration :
KEY VALUE
array:entries:3 value3
If the ArrayExamples class instance is bound after the JSON Configuration Provider includes the entry for index #3,
the ArrayExamples.Entries array includes the value.
0 value0
1 value1
ARRAYEXAMPLES.ENTRIES INDEX ARRAYEXAMPLES.ENTRIES VALUE
2 value2
3 value3
4 value4
5 value5
{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}
{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}
The JSON Configuration Provider reads the configuration data into the following key-value pairs:
KEY VALUE
json_array:key valueA
json_array:subsection:0 valueB
json_array:subsection:1 valueC
json_array:subsection:2 valueD
In the sample app, the following POCO class is available to bind the configuration key-value pairs:
public class JsonArrayExample
{
public string Key { get; set; }
public string[] Subsection { get; set; }
}
After binding, JsonArrayExample.Key holds the value valueA . The subsection values are stored in the POCO array
property, Subsection .
0 valueB
1 valueC
2 valueD
Create the custom configuration provider by inheriting from ConfigurationProvider. The configuration provider
initializes the database when it's empty.
EFConfigurationProvider/EFConfigurationProvider.cs:
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}
OptionsAction(builder);
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}
dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}
OptionsAction(builder);
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}
dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
The following code shows how to use the custom EFConfigurationProvider in Program.cs:
Configuration = builder.Build();
}
For an example of accessing configuration using startup convenience methods, see App startup: Convenience
methods.
@page
@model IndexModel
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index Page</title>
</head>
<body>
<h1>Access configuration in a Razor Pages page</h1>
<p>Configuration value for 'key': @Configuration["key"]</p>
</body>
</html>
In an MVC view:
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index View</title>
</head>
<body>
<h1>Access configuration in an MVC view</h1>
<p>Configuration value for 'key': @Configuration["key"]</p>
</body>
</html>
Additional resources
Options pattern in ASP.NET Core
Deep Dive into Microsoft Configuration
Configuration in ASP.NET Core
9/6/2018 • 31 minutes to read • Edit Online
By Luke Latham
App configuration in ASP.NET Core is based on key-value pairs established by configuration providers.
Configuration providers read configuration data into key-value pairs from a variety of configuration
sources:
Azure Key Vault
Command-line arguments
Custom providers (installed or created)
Directory files
Environment variables
In-memory .NET objects
Settings files
Azure Key Vault
Command-line arguments
Custom providers (installed or created)
Environment variables
In-memory .NET objects
Settings files
Command-line arguments
Custom providers (installed or created)
Environment variables
In-memory .NET objects
Settings files
The options pattern is an extension of the configuration concepts described in this topic. Options uses
classes to represent groups of related settings. For more information on using the options pattern, see
Options pattern in ASP.NET Core.
View or download sample code (how to download)
The examples provided in this topic rely upon:
Setting the base path of the app with SetBasePath. SetBasePath is made available to an app by
referencing the Microsoft.Extensions.Configuration.FileExtensions package.
Resolving sections of configuration files with GetSection. GetSection is made available to an app by
referencing the Microsoft.Extensions.Configuration package.
Binding configuration to .NET classes with Bind and Get<T>. Bind and Get<T> are made available to
an app by referencing the Microsoft.Extensions.Configuration.Binder package. Get<T> is available in
ASP.NET Core 1.1 or later.
These three packages are included in the Microsoft.AspNetCore.App metapackage.
These three packages are included in the Microsoft.AspNetCore.All metapackage.
Host vs. app configuration
Before the app is configured and started, a host is configured and launched. The host is responsible for app
startup and lifetime management. Both the app and the host are configured using the configuration
providers described in this topic. Host configuration key-value pairs become part of the app's global
configuration. For more information on how the configuration providers are used when the host is built and
how configuration sources affect host configuration, see Host in ASP.NET Core.
Security
Adopt the following best practices:
Never store passwords or other sensitive data in configuration provider code or in plain text
configuration files.
Don't use production secrets in development or test environments.
Specify secrets outside of the project so that they can't be accidentally committed to a source code
repository.
Learn more about how to use multiple environments and managing the safe storage of app secrets in
development with the Secret Manager (includes advice on using environment variables to store sensitive
data). The Secret Manager uses the File Configuration Provider to store user secrets in a JSON file on the
local system. The File Configuration Provider is described later in this topic.
Azure Key Vault is one option for the safe storage of app secrets. For more information, see Azure Key Vault
configuration provider in ASP.NET Core.
{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
}
}
When the file is read into configuration, unique keys are created to maintain the original hierarchical data
structure of the configuration source. The sections and keys are flattened with the use of a colon ( : ) to
maintain the original structure:
section0:key0
section0:key1
section1:key0
section1:key1
GetSection and GetChildren methods are available to isolate sections and children of a section in the
configuration data. These methods are described later in GetSection, GetChildren, and Exists.
Conventions
At app startup, configuration sources are read in the order that their configuration providers are specified.
File Configuration Providers have the ability to reload configuration when an underlying settings file is
changed after app startup. The File Configuration Provider is described later in this topic.
IConfiguration is available in the app's Dependency Injection (DI) container. Configuration providers can't
utilize DI, as it's not available when they're set up by the host.
Configuration keys adopt the following conventions:
Keys are case-insensitive. For example, ConnectionString and connectionstring are treated as
equivalent keys.
If a value for the same key is set by the same or different configuration providers, the last value set on
the key is the value used.
Hierarchical keys
Within the Configuration API, a colon separator ( : ) works on all platforms.
In environment variables, a colon separator may not work on all platforms. A double underscore (
__ ) is supported by all platforms and is converted to a colon.
In Azure Key Vault, hierarchical keys use -- (two dashes) as a separator. You must provide code
to replace the dashes with a colon when the secrets are loaded into the app's configuration.
The ConfigurationBinder supports binding arrays to objects using array indices in configuration keys.
Array binding is described in the Bind an array to a class section.
Configuration values adopt the following conventions:
Values are strings.
Null values can't be stored in configuration or bound to objects.
Providers
The following table shows the configuration providers available to ASP.NET Core apps.
Azure Key Vault Configuration Provider (Security topics) Azure Key Vault
User secrets (Secret Manager) (Security topics) File in the user profile directory
PROVIDER PROVIDES CONFIGURATION FROM…
Azure Key Vault Configuration Provider (Security topics) Azure Key Vault
User secrets (Secret Manager) (Security topics) File in the user profile directory
User secrets (Secret Manager) (Security topics) File in the user profile directory
Configuration sources are read in the order that their configuration providers are specified at startup. The
configuration providers described in this topic are described in alphabetical order, not in the order that your
code may arrange them. Order configuration providers in your code to suit your priorities for the
underlying configuration sources.
A typical sequence of configuration providers is:
1. Files (appsettings.json, appsettings.<Environment>.json, where <Environment> is the app's current
hosting environment)
2. User secrets (Secret Manager) (in the Development environment only)
3. Environment variables
4. Command-line arguments
It's a common practice to position the Command-line Configuration Provider last in a series of providers to
allow command-line arguments to override configuration set by the other providers.
This sequence of providers is put into place when you initialize a new WebHostBuilder with
CreateDefaultBuilder. For more information, see Web Host: Set up a host.
Call ConfigureAppConfiguration when building the Web Host to specify the app's configuration providers:
public class Program
{
public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
{
{"array:entries:0", "value0"},
{"array:entries:1", "value1"},
{"array:entries:2", "value2"},
{"array:entries:4", "value4"},
{"array:entries:5", "value5"}
};
if (appAssembly != null)
{
builder.AddUserSecrets(appAssembly, optional: true);
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
In the preceding example, the environment name ( env.EnvironmentName ) and app assembly name (
env.ApplicationName) are provided by the IHostingEnvironment. For more information, see Use multiple
environments in ASP.NET Core.
Example
The 2.x sample app takes advantage of the static convenience method CreateDefaultBuilder to build the
host, which includes a call to AddCommandLine.
The 1.x sample app calls AddCommandLine on a ConfigurationBuilder.
1. Open a command prompt in the project's directory.
2. Supply a command-line argument to the dotnet run command,
dotnet run CommandLineKey=CommandLineValue .
3. After the app is running, open a browser to the app at http://localhost:5000 .
4. Observe that the output contains the key-value pair for the configuration command-line argument
provided to dotnet run .
Arguments
The value must follow an equals sign ( = ), or the key must have a prefix ( -- or / ) when the value follows
a space. The value can be null if an equals sign is used (for example, CommandLineKey= ).
No prefix CommandLineKey1=value1
Within the same command, don't mix command-line argument key-value pairs that use an equals sign with
key-value pairs that use a space.
Example commands:
Switch mappings
Switch mappings allow key name replacement logic. When you manually build configuration with a
ConfigurationBuilder, you can provide a dictionary of switch replacements to the AddCommandLine
method.
When the switch mappings dictionary is used, the dictionary is checked for a key that matches the key
provided by a command-line argument. If the command-line key is found in the dictionary, the dictionary
value (the key replacement) is passed back to set the key-value pair into the app's configuration. A switch
mapping is required for any command-line key prefixed with a single dash ( - ).
Switch mappings dictionary key rules:
Switches must start with a dash ( - ) or double-dash ( -- ).
The switch mappings dictionary must not contain duplicate keys.
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
return WebHost.CreateDefaultBuilder()
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
As shown in the preceding example, the call to CreateDefaultBuilder shouldn't pass arguments when
switch mappings are used. CreateDefaultBuilder method's AddCommandLine call doesn't include mapped
switches, and there's no way to pass the switch mapping dictionary to CreateDefaultBuilder . If the
arguments include a mapped switch and are passed to CreateDefaultBuilder , its AddCommandLine provider
fails to initialize with a FormatException. The solution is not to pass the arguments to CreateDefaultBuilder
but instead to allow the ConfigurationBuilder method's AddCommandLine method to process both the
arguments and the switch mapping dictionary.
using (host)
{
Console.ReadLine();
}
}
After the switch mappings dictionary is created, it contains the data shown in the following table.
KEY VALUE
-CLKey1 CommandLineKey1
-CLKey2 CommandLineKey2
If the switch-mapped keys are used when starting the app, configuration receives the configuration value
on the key supplied by the dictionary:
After running the preceding command, configuration contains the values shown in the following table.
KEY VALUE
CommandLineKey1 value1
CommandLineKey2 value2
Example
The 2.x sample app takes advantage of the static convenience method CreateDefaultBuilder to build the
host, which includes a call to AddEnvironmentVariables .
The 1.x sample app calls AddEnvironmentVariables on a ConfigurationBuilder .
1. Run the sample app. Open a browser to the app at http://localhost:5000 .
2. Observe that the output contains the key-value pair for the environment variable ENVIRONMENT . The
value reflects the environment in which the app is running, typically Development when running locally.
To keep the list of environment variables rendered by the app short, the app filters environment variables to
those that start with the following:
ASPNETCORE_
urls
Logging
ENVIRONMENT
contentRoot
AllowedHosts
applicationName
CommandLine
If you wish to expose all of the environment variables available to the app, change the
FilteredConfiguration in Pages/Index.cshtml.cs to the following:
If you wish to expose all of the environment variables available to the app, change the
FilteredConfiguration in Controllers/HomeController.cs to the following:
FilteredConfiguration = _config.AsEnumerable();
Prefixes
Environment variables loaded into the app's configuration are filtered when you supply a prefix to the
AddEnvironmentVariables method. For example, to filter environment variables on the prefix CUSTOM_ ,
supply the prefix to the configuration provider:
The prefix is stripped off when the configuration key-value pairs are created.
The static convenience method CreateDefaultBuilder creates a WebHostBuilder to establish the app's host.
When WebHostBuilder is created, it finds its host configuration in environment variables prefixed with
ASPNETCORE_ .
Connection string prefixes
The Configuration API has special processing rules for four connection string environment variables
involved in configuring Azure connection strings for the app environment. Environment variables with the
prefixes shown in the table are loaded into the app if no prefix is supplied to AddEnvironmentVariables .
MYSQLCONNSTR_ MySQL
When an environment variable is discovered and loaded into configuration with any of the four prefixes
shown in the table:
The configuration key is created by removing the environment variable prefix and adding a
configuration key section ( ConnectionStrings ).
A new configuration key-value pair is created that represents the database connection provider (except
for CUSTOMCONNSTR_ , which has no stated provider).
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
[section1]
subsection:key=value
[section2:subsection0]
key=value
[section2:subsection1]
key=value
The previous configuration file loads the following keys with value :
section0:key0
section0:key1
section1:subsection:key
section2:subsection0:key
section2:subsection1:key
JSON Configuration Provider
The JsonConfigurationProvider loads configuration from JSON file key-value pairs during runtime.
To activate JSON file configuration, call the AddJsonFile extension method on an instance of
ConfigurationBuilder.
Overloads permit specifying:
Whether the file is optional.
Whether the configuration is reloaded if the file changes.
The IFileProvider used to access the file.
AddJsonFile is automatically called twice when you initialize a new WebHostBuilder with
CreateDefaultBuilder. The method is called to load configuration from:
appsettings.json – This file is read first. The environment version of the file can override the values
provided by the appsettings.json file.
appsettings.<Environment>.json – The environment version of the file is loaded based on the
IHostingEnvironment.EnvironmentName.
For more information, see Web Host: Set up a host.
CreateDefaultBuilder also loads:
Environment variables.
User secrets (Secret Manager) (in the Development environment).
Command-line arguments.
The JSON Configuration Provider is established first. Therefore, user secrets, environment variables, and
command-line arguments override configuration set by the appsettings files.
You can also directly call the AddJsonFile extension method on an instance of ConfigurationBuilder.
When calling CreateDefaultBuilder , call UseConfiguration with the configuration:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
Example
The 2.x sample app takes advantage of the static convenience method CreateDefaultBuilder to build the
host, which includes two calls to AddJsonFile . Configuration is loaded from appsettings.json and
appsettings.<Environment>.json.
The 1.x sample app calls AddJsonFile twice on a ConfigurationBuilder . Configuration is loaded from
appsettings.json and appsettings.<Environment>.json.
1. Run the sample app. Open a browser to the app at http://localhost:5000 .
2. Observe that the output contains key-value pairs for the configuration shown in the table depending on
the environment. Logging configuration keys use the colon ( : ) as a hierarchical separator.
AllowedHosts * *
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
XML configuration files can use distinct element names for repeating sections:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<section0>
<key0>value</key0>
<key1>value</key1>
</section0>
<section1>
<key0>value</key0>
<key1>value</key1>
</section1>
</configuration>
The previous configuration file loads the following keys with value :
section0:key0
section0:key1
section1:key0
section1:key1
Repeating elements that use the same element name work if the name attribute is used to distinguish the
elements:
The previous configuration file loads the following keys with value :
section:section0:key:key0
section:section0:key:key1
section:section1:key:key0
section:section1:key:key1
Attributes can be used to supply values:
The previous configuration file loads the following keys with value :
key:attribute
section:key:attribute
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
return WebHost.CreateDefaultBuilder(args)
.UseConfiguration(config)
.UseStartup<Startup>();
}
}
GetValue
ConfigurationBinder.GetValue<T> extracts a value from configuration with a specified key and converts it
to the specified type. An overload permits you to provide a default value if the key isn't found.
The following example extracts the string value from configuration with the key NumberKey , types the value
as an int , and stores the value in the variable intValue . If NumberKey isn't found in the configuration keys,
intValue receives the default value of 99 :
When the file is read into configuration, the following unique hierarchical keys are created to hold the
configuration values:
section0:key0
section0:key1
section1:key0
section1:key1
section2:subsection0:key0
section2:subsection0:key1
section2:subsection1:key0
section2:subsection1:key1
GetSection
IConfiguration.GetSection extracts a configuration subsection with the specified subsection key.
To return an IConfigurationSection containing only the key-value pairs in section1 , call GetSection and
supply the section name:
Similarly, to obtain the values for keys in section2:subsection0 , call GetSection and supply the section
path:
GetSection never returns null . If a matching section isn't found, an empty IConfigurationSection is
returned.
GetChildren
A call to IConfiguration.GetChildren on section2 obtains an IEnumerable<IConfigurationSection> that
includes:
subsection0
subsection1
var configSection = _config.GetSection("section2");
Exists
Use ConfigurationExtensions.Exists to determine if a configuration section exists:
Given the example data, sectionExists is false because there isn't a section2:subsection2 section in the
configuration data.
Bind to a class
Configuration can be bound to classes that represent groups of related settings using the options pattern.
For more information, see Options pattern in ASP.NET Core.
Configuration values are returned as strings, but calling Bind enables the construction of POCO objects.
The sample app contains a Starship model (Models/Starship.cs):
The starship section of the starship.json file creates the configuration when the sample app uses the JSON
Configuration Provider to load the configuration:
{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}
{
"starship": {
"name": "USS Enterprise",
"registry": "NCC-1701",
"class": "Constitution",
"length": 304.8,
"commissioned": false
},
"trademark": "Paramount Pictures Corp. http://www.paramount.com"
}
KEY VALUE
starship:registry NCC-1701
starship:class Constitution
starship:length 304.8
starship:commissioned False
The sample app calls GetSection with the starship key. The starship key-value pairs are isolated. The
Bind method is called on the subsection passing in an instance of the Starship class. After binding the
instance values, the instance is assigned to a property for rendering:
The sample app has a tvshow.xml file containing the configuration data:
Configuration is bound to the entire TvShow object graph with the Bind method. The bound instance is
assigned to a property for rendering:
ConfigurationBinder.Get<T> binds and returns the specified type. Get<T> is more convenient than using
Bind . The following code shows how to use Get<T> with the preceding example, which allows the bound
instance to be directly assigned to the property used for rendering:
TvShow = _config.GetSection("tvshow").Get<TvShow>();
viewModel.TvShow = _config.GetSection("tvshow").Get<TvShow>();
NOTE
Binding is provided by convention. Custom configuration providers aren't required to implement array binding.
KEY VALUE
array:0 value0
KEY VALUE
array:1 value1
array:2 value2
array:4 value4
array:5 value5
These keys and values are loaded in the sample app using the Memory Configuration Provider:
Configuration = builder.Build();
}
The array skips a value for index #3. The configuration binder isn't capable of binding null values or creating
null entries in bound objects, which becomes clear in a moment when the result of binding this array to an
object is demonstrated.
In the sample app, a POCO class is available to hold the bound configuration data:
ConfigurationBinder.Get<T> syntax can also be used, which results in more compact code:
ArrayExample = _config.GetSection("array").Get<ArrayExample>();
viewModel.ArrayExample = _config.GetSection("array").Get<ArrayExample>();
The bound object, an instance of ArrayExample , receives the array data from configuration.
ARRAYEXAMPLES.ENTRIES INDEX ARRAYEXAMPLES.ENTRIES VALUE
0 value0
1 value1
2 value2
3 value4
4 value5
Index #3 in the bound object holds the configuration data for the array:4 configuration key and its value
of value4 . When configuration data containing an array is bound, the array indices in the configuration
keys are merely used to iterate the configuration data when creating the object. A null value can't be
retained in configuration data, and a null-valued entry isn't created in a bound object when an array in
configuration keys skip one or more indices.
The missing configuration item for index #3 can be supplied before binding to the ArrayExamples instance
by any configuration provider that produces the correct key-value pair in configuration. If the sample
included an additional JSON Configuration Provider with the missing key-value pair, the
ArrayExamples.Entries matches the complete configuration array:
missing_value.json:
{
"array:entries:3": "value3"
}
In ConfigureAppConfiguration :
KEY VALUE
array:entries:3 value3
If the ArrayExamples class instance is bound after the JSON Configuration Provider includes the entry for
index #3, the ArrayExamples.Entries array includes the value.
0 value0
1 value1
ARRAYEXAMPLES.ENTRIES INDEX ARRAYEXAMPLES.ENTRIES VALUE
2 value2
3 value3
4 value4
5 value5
{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}
{
"json_array": {
"key": "valueA",
"subsection": [
"valueB",
"valueC",
"valueD"
]
}
}
The JSON Configuration Provider reads the configuration data into the following key-value pairs:
KEY VALUE
json_array:key valueA
json_array:subsection:0 valueB
json_array:subsection:1 valueC
json_array:subsection:2 valueD
In the sample app, the following POCO class is available to bind the configuration key-value pairs:
public class JsonArrayExample
{
public string Key { get; set; }
public string[] Subsection { get; set; }
}
After binding, JsonArrayExample.Key holds the value valueA . The subsection values are stored in the POCO
array property, Subsection .
0 valueB
1 valueC
2 valueD
Create the custom configuration provider by inheriting from ConfigurationProvider. The configuration
provider initializes the database when it's empty.
EFConfigurationProvider/EFConfigurationProvider.cs:
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}
OptionsAction(builder);
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}
dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
public class EFConfigurationProvider : ConfigurationProvider
{
public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
{
OptionsAction = optionsAction;
}
OptionsAction(builder);
Data = !dbContext.Values.Any()
? CreateAndSaveDefaultValues(dbContext)
: dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
}
}
dbContext.Values.AddRange(configValues
.Select(kvp => new EFConfigurationValue
{
Id = kvp.Key,
Value = kvp.Value
})
.ToArray());
dbContext.SaveChanges();
return configValues;
}
}
The following code shows how to use the custom EFConfigurationProvider in Program.cs:
Configuration = builder.Build();
}
For an example of accessing configuration using startup convenience methods, see App startup:
Convenience methods.
@page
@model IndexModel
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index Page</title>
</head>
<body>
<h1>Access configuration in a Razor Pages page</h1>
<p>Configuration value for 'key': @Configuration["key"]</p>
</body>
</html>
In an MVC view:
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
<!DOCTYPE html>
<html lang="en">
<head>
<title>Index View</title>
</head>
<body>
<h1>Access configuration in an MVC view</h1>
<p>Configuration value for 'key': @Configuration["key"]</p>
</body>
</html>
Additional resources
Options pattern in ASP.NET Core
Deep Dive into Microsoft Configuration
Options pattern in ASP.NET Core
8/25/2018 • 11 minutes to read • Edit Online
By Luke Latham
The options pattern uses classes to represent groups of related settings. When configuration settings are isolated
by scenario into separate classes, the app adheres to two important software engineering principles:
The Interface Segregation Principle (ISP ): Scenarios (classes) that depend on configuration settings depend
only on the configuration settings that they use.
Separation of Concerns: Settings for different parts of the app aren't dependent or coupled to one another.
View or download sample code (how to download) This article is easier to follow with the sample app.
Prerequisites
Reference the Microsoft.AspNetCore.App metapackage or add a package reference to the
Microsoft.Extensions.Options.ConfigurationExtensions package.
Reference the Microsoft.AspNetCore.All metapackage or add a package reference to the
Microsoft.Extensions.Options.ConfigurationExtensions package.
Add a package reference to the Microsoft.Extensions.Options.ConfigurationExtensions package.
The following page model uses constructor dependency injection with IOptions<TOptions> to access the
settings (Pages/Index.cshtml.cs):
public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
The sample's appsettings.json file specifies values for option1 and option2 :
{
"option1": "value1_from_json",
"option2": -1,
"subsection": {
"suboption1": "subvalue1_from_json",
"suboption2": 200
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
When the app is run, the page model's OnGet method returns a string showing the option class values:
services.Configure<MyOptions>(config);
Explicitly setting the base path isn't required when loading options configuration from the settings file via
CreateDefaultBuilder.
In the following code, a second IConfigureOptions<TOptions> service is added to the service container. It uses a
delegate to configure the binding with MyOptionsWithDelegateConfig :
Index.cshtml.cs:
You can add multiple configuration providers. Configuration providers are available in NuGet packages. They're
applied in order that they're registered.
Each call to Configure<TOptions> adds an IConfigureOptions<TOptions> service to the service container. In the
preceding example, the values of Option1 and Option2 are both specified in appsettings.json, but the values of
Option1 and Option2 are overridden by the configured delegate.
When more than one configuration service is enabled, the last configuration source specified wins and sets the
configuration value. When the app is run, the page model's OnGet method returns a string showing the option
class values:
Suboptions configuration
Suboptions configuration is demonstrated as Example #3 in the sample app.
Apps should create options classes that pertain to specific scenario groups (classes) in the app. Parts of the app
that require configuration values should only have access to the configuration values that they use.
When binding options to configuration, each property in the options type is bound to a configuration key of the
form property[:sub-property:] . For example, the MyOptions.Option1 property is bound to the key Option1 ,
which is read from the option1 property in appsettings.json.
In the following code, a third IConfigureOptions<TOptions> service is added to the service container. It binds
MySubOptions to the section subsection of the appsettings.json file:
{
"option1": "value1_from_json",
"option2": -1,
"subsection": {
"suboption1": "subvalue1_from_json",
"suboption2": 200
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
The MySubOptions class defines properties, SubOption1 and SubOption2 , to hold the options values
(Models/MySubOptions.cs):
The page model's OnGet method returns a string with the options values (Pages/Index.cshtml.cs):
public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
When the app is run, the OnGet method returns a string showing the sub-option class values:
subOption1 = subvalue1_from_json, subOption2 = 200
public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
@page
@model IndexModel
@using Microsoft.Extensions.Options
@using UsingOptionsSample.Models
@inject IOptions<MyOptions> OptionsAccessor
@{
ViewData["Title"] = "Using Options Sample";
}
<h1>@ViewData["Title"]</h1>
When the app is run, the options values are shown in the rendered page:
Reload configuration data with IOptionsSnapshot
Reloading configuration data with IOptionsSnapshot is demonstrated in Example #5 in the sample app.
IOptionsSnapshot supports reloading options with minimal processing overhead.
Options are computed once per request when accessed and cached for the lifetime of the request.
IOptionsSnapshot is a snapshot of IOptionsMonitor<TOptions> and updates automatically whenever the
monitor triggers changes based on the data source changing.
The following example demonstrates how a new IOptionsSnapshot is created after appsettings.json changes
(Pages/Index.cshtml.cs). Multiple requests to the server return constant values provided by the appsettings.json
file until the file is changed and configuration reloads.
public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
The following image shows the initial option1 and option2 values loaded from the appsettings.json file:
snapshot option1 = value1_from_json, snapshot option2 = -1
Change the values in the appsettings.json file to value1_from_json UPDATED and 200 . Save the appsettings.json
file. Refresh the browser to see that the options values are updated:
The sample app accesses the named options with IOptionsSnapshot<TOptions>.Get (Pages/Index.cshtml.cs):
public IndexModel(
IOptions<MyOptions> optionsAccessor,
IOptions<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig,
IOptions<MySubOptions> subOptionsAccessor,
IOptionsSnapshot<MyOptions> snapshotOptionsAccessor,
IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
_options = optionsAccessor.Value;
_optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.Value;
_subOptions = subOptionsAccessor.Value;
_snapshotOptions = snapshotOptionsAccessor.Value;
_named_options_1 = namedOptionsAccessor.Get("named_options_1");
_named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #6: Named options
var named_options_1 =
$"named_options_1: option1 = {_named_options_1.Option1}, " +
$"option2 = {_named_options_1.Option2}";
var named_options_2 =
$"named_options_2: option1 = {_named_options_2.Option1}, " +
$"option2 = {_named_options_2.Option2}";
NamedOptions = $"{named_options_1} {named_options_2}";
named_options_1 values are provided from configuration, which are loaded from the appsettings.json file.
named_options_2 values are provided by:
The named_options_2 delegate in ConfigureServices for Option1 .
The default value for Option2 provided by the MyOptions class.
Configure all named options instances with the OptionsServiceCollectionExtensions.ConfigureAll method. The
following code configures Option1 for all named configuration instances with a common value. Add the
following code manually to the Configure method:
services.ConfigureAll<MyOptions>(myOptions =>
{
myOptions.Option1 = "ConfigureAll replacement value";
});
Running the sample app after adding the code produces the following result:
NOTE
All options are named instances. Existing IConfigureOption instances are treated as targeting the
Options.DefaultName instance, which is string.Empty . IConfigureNamedOptions also implements
IConfigureOptions . The default implementation of the IOptionsFactory<TOptions> (reference source has logic to use
each appropriately. The null named option is used to target all of the named instances instead of a specific named
instance (ConfigureAll and PostConfigureAll use this convention).
Options validation
Options validation allows you to validate options when options are configured. Call Validate with a validation
method that returns true if options are valid and false if they aren't valid:
// Registration
services.AddOptions<MyOptions>("optionalOptionsName")
.Configure(o => { }) // Configure the options
.Validate(o => YourValidationShouldReturnTrueIfValid(o),
"custom error");
// Consumption
var monitor = services.BuildServiceProvider()
.GetService<IOptionsMonitor<MyOptions>>();
try
{
var options = monitor.Get("optionalOptionsName");
}
catch (OptionsValidationException e)
{
// e.OptionsName returns "optionalOptionsName"
// e.OptionsType returns typeof(MyOptions)
// e.Failures returns a list of errors, which would contain
// "custom error"
}
The preceding example sets the named options instance to optionalOptionsName . The default options instance is
Options.DefaultName .
Validation runs when the options instance is created. Your options instance is guaranteed to pass validation the
first time it's accessed.
IMPORTANT
Options validation doesn't guard against options modifications after the options are initially configured and validated.
IValidateOptions validates:
A specific named options instance.
All options when name is null .
Eager validation (fail fast at startup) and data annotation-based validation are scheduled for a future release.
IPostConfigureOptions
Set postconfiguration with IPostConfigureOptions<TOptions>. Postconfiguration runs after all
IConfigureOptions<TOptions> configuration occurs:
services.PostConfigure<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});
services.PostConfigureAll<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});
IOptions shouldn't be used in Startup.ConfigureServices . An inconsistent options state may exist due to the
ordering of service registrations.
Additional resources
Configuration in ASP.NET Core
Logging in ASP.NET Core
9/20/2018 • 24 minutes to read • Edit Online
This example creates logs with the TodoController class as the category. Categories are explained later in this
article.
ASP.NET Core doesn't provide async logger methods because logging should be so fast that it isn't worth the
cost of using async. If you're in a situation where that's not true, consider changing the way you log. If your data
store is slow, write the log messages to a fast store first, then move them to a slow store later. For example, log
to a message queue that's read and persisted to slow storage by another process.
webHost.Run();
}
The default project template enables the Console and Debug logging providers with a call to the
CreateDefaultBuilder extension method in Program.cs:
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
A logging provider takes the messages that you create with an ILogger object and displays or stores them. For
example, the Console provider displays messages on the console, and the Azure App Service provider can store
them in Azure blob storage.
To use a provider, install its NuGet package and call the provider's extension method on an instance of
ILoggerFactory, as shown in the following example:
ASP.NET Core dependency injection (DI) provides the ILoggerFactory instance. The AddConsole and AddDebug
extension methods are defined in the Microsoft.Extensions.Logging.Console and
Microsoft.Extensions.Logging.Debug packages. Each extension method calls the ILoggerFactory.AddProvider
method, passing in an instance of the provider.
NOTE
The 1.x sample app adds logging providers in the Startup.Configure method. If you want to obtain log output from
code that executes earlier, add logging providers in the Startup class constructor.
Learn more about the built-in logging providers and find links to third-party logging providers later in the
article.
Configuration
Logging provider configuration is provided by one or more configuration providers:
File formats (INI, JSON, and XML ).
Command-line arguments.
Environment variables.
In-memory .NET objects.
The unencrypted Secret Manager storage.
An encrypted user store, such as Azure Key Vault.
Custom providers (installed or created).
For example, logging configuration is commonly provided by the Logging section of app settings files. The
following example shows the contents of a typical appsettings.Development.json file:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
},
"Console":
{
"IncludeScopes": true
}
}
}
LogLevel keys represent log names. The Default key applies to logs not explicitly listed. The value represents
the log level applied to the given log. Log keys that set IncludeScopes ( Console in the example), specify if log
scopes are enabled for the indicated log.
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
LogLevel keys represent log names. The Default key applies to logs not explicitly listed. The value represents
the log level applied to the given log.
For information on implementing configuration providers, see Configuration in ASP.NET Core.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:5000/api/todo/0
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method TodoApi.Controllers.TodoController.GetById (TodoApi) with arguments (0) -
ModelState is Valid
info: TodoApi.Controllers.TodoController[1002]
Getting item 0
warn: TodoApi.Controllers.TodoController[4000]
GetById(0) NOT FOUND
info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
Executing HttpStatusCodeResult, setting HTTP status code 404
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action TodoApi.Controllers.TodoController.GetById (TodoApi) in 42.9286ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 148.889ms 404
These logs were created by going to http://localhost:5000/api/todo/0 , which triggers execution of both
ILogger calls shown in the preceding section.
Here's an example of the same logs as they appear in the Debug window when you run the sample application
in Visual Studio:
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET
http://localhost:53104/api/todo/0
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executing action method
TodoApi.Controllers.TodoController.GetById (TodoApi) with arguments (0) - ModelState is Valid
TodoApi.Controllers.TodoController:Information: Getting item 0
TodoApi.Controllers.TodoController:Warning: GetById(0) NOT FOUND
Microsoft.AspNetCore.Mvc.StatusCodeResult:Information: Executing HttpStatusCodeResult, setting HTTP status
code 404
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action
TodoApi.Controllers.TodoController.GetById (TodoApi) in 152.5657ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 316.3195ms 404
The logs that were created by the ILogger calls shown in the preceding section begin with
"TodoApi.Controllers.TodoController". The logs that begin with "Microsoft" categories are from ASP.NET Core.
ASP.NET Core itself and your application code are using the same logging API and the same logging providers.
The remainder of this article explains some details and options for logging.
NuGet packages
The ILogger and ILoggerFactory interfaces are in Microsoft.Extensions.Logging.Abstractions, and default
implementations for them are in Microsoft.Extensions.Logging.
Log category
A category is included with each log that you create. You specify the category when you create an ILogger
object. The category may be any string, but a convention is to use the fully qualified name of the class from
which the logs are written. For example: "TodoApi.Controllers.TodoController".
You can specify the category as a string or use an extension method that derives the category from the type. To
specify the category as a string, call CreateLogger on an ILoggerFactory instance, as shown below.
Most of the time, it will be easier to use ILogger<T> , as in the following example.
public class TodoController : Controller
{
private readonly ITodoRepository _todoRepository;
private readonly ILogger _logger;
This is equivalent to calling CreateLogger with the fully qualified type name of T .
Log level
Each time you write a log, you specify its LogLevel. The log level indicates the degree of severity or importance.
For example, you might write an Information log when a method ends normally, a Warning log when a method
returns a 404 return code, and an Error log when you catch an unexpected exception.
In the following code example, the names of the methods (for example, LogWarning ) specify the log level. The
first parameter is the Log event ID. The second parameter is a message template with placeholders for
argument values provided by the remaining method parameters. The method parameters are explained in more
detail later in this article.
Log methods that include the level in the method name are extension methods for ILogger. Behind the scenes,
these methods call a Log method that takes a LogLevel parameter. You can call the Log method directly rather
than one of these extension methods, but the syntax is relatively complicated. For more information, see the
ILogger interface and the logger extensions source code.
ASP.NET Core defines the following log levels, ordered here from least to highest severity.
Trace = 0
For information that's valuable only to a developer debugging an issue. These messages may contain
sensitive application data and so shouldn't be enabled in a production environment. Disabled by default.
Example: Credentials: {"User":"someuser", "Password":"P@ssword"}
Debug = 1
For information that has short-term usefulness during development and debugging. Example:
Entering method Configure with flag set to true. You typically wouldn't enable Debug level logs in
production unless you are troubleshooting, due to the high volume of logs.
Information = 2
For tracking the general flow of the application. These logs typically have some long-term value. Example:
Request received for path /api/todo
Warning = 3
For abnormal or unexpected events in the application flow. These may include errors or other conditions
that don't cause the application to stop, but which may need to be investigated. Handled exceptions are a
common place to use the Warning log level. Example: FileNotFoundException for file quotes.txt.
Error = 4
For errors and exceptions that cannot be handled. These messages indicate a failure in the current activity
or operation (such as the current HTTP request), not an application-wide failure. Example log message:
Cannot insert record due to duplicate key violation.
Critical = 5
For failures that require immediate attention. Examples: data loss scenarios, out of disk space.
You can use the log level to control how much log output is written to a particular storage medium or display
window. For example, in production you might want all logs of Information level and lower to go to a volume
data store, and all logs of Warning level and higher to go to a value data store. During development, you might
normally send logs of Warning or higher severity to the console. Then when you need to troubleshoot, you can
add Debug level. The Log filtering section later in this article explains how to control which log levels a provider
handles.
The ASP.NET Core framework writes Debug level logs for framework events. The log examples earlier in this
article excluded logs below Information level, so no Debug level logs were shown. Here's an example of
console logs if you run the sample application configured to show Debug and higher logs for the console
provider.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET http://localhost:62555/api/todo/0
dbug: Microsoft.AspNetCore.Routing.Tree.TreeRouter[1]
Request successfully matched the route with name 'GetTodo' and template 'api/Todo/{id}'.
dbug: Microsoft.AspNetCore.Mvc.Internal.ActionSelector[2]
Action 'TodoApi.Controllers.TodoController.Update (TodoApi)' with id '089d59b6-92ec-472d-b552-
cc613dfd625d' did not match the constraint 'Microsoft.AspNetCore.Mvc.Internal.HttpMethodActionConstraint'
dbug: Microsoft.AspNetCore.Mvc.Internal.ActionSelector[2]
Action 'TodoApi.Controllers.TodoController.Delete (TodoApi)' with id 'f3476abe-4bd9-4ad3-9261-
3ead09607366' did not match the constraint 'Microsoft.AspNetCore.Mvc.Internal.HttpMethodActionConstraint'
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action TodoApi.Controllers.TodoController.GetById (TodoApi)
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method TodoApi.Controllers.TodoController.GetById (TodoApi) with arguments (0) -
ModelState is Valid
info: TodoApi.Controllers.TodoController[1002]
Getting item 0
warn: TodoApi.Controllers.TodoController[4000]
GetById(0) NOT FOUND
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action method TodoApi.Controllers.TodoController.GetById (TodoApi), returned result
Microsoft.AspNetCore.Mvc.NotFoundResult.
info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
Executing HttpStatusCodeResult, setting HTTP status code 404
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action TodoApi.Controllers.TodoController.GetById (TodoApi) in 0.8788ms
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
Connection id "0HL6L7NEFF2QD" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 2.7286ms 404
Log event ID
Each time you write a log, you can specify an event ID. The sample app does this by using a locally-defined
LoggingEvents class:
An event ID is an integer value that you can use to associate a set of logged events with one another. For
instance, a log for adding an item to a shopping cart could be event ID 1000 and a log for completing a
purchase could be event ID 1001.
In logging output, the event ID may be stored in a field or included in the text message, depending on the
provider. The Debug provider doesn't show event IDs, but the console provider shows them in brackets after the
category:
info: TodoApi.Controllers.TodoController[1002]
Getting item invalidid
warn: TodoApi.Controllers.TodoController[4000]
GetById(invalidid) NOT FOUND
The order of placeholders, not their names, determines which parameters are used to provide their values. If you
have the following code:
string p1 = "parm1";
string p2 = "parm2";
_logger.LogInformation("Parameter values: {p2}, {p1}", p1, p2);
The logging framework does message formatting in this way to make it possible for logging providers to
implement semantic logging, also known as structured logging. Because the arguments themselves are passed
to the logging system, not just the formatted message template, logging providers can store the parameter
values as fields in addition to the message template. If you're directing your log output to Azure Table Storage
and your logger method call looks like this:
Each Azure Table entity can have ID and RequestTime properties, which simplifies queries on log data. You can
find all logs within a particular RequestTime range without the need to parse the time out of the text message.
Logging exceptions
The logger methods have overloads that let you pass in an exception, as in the following example:
catch (Exception ex)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, ex, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
return new ObjectResult(item);
Different providers handle the exception information in different ways. Here's an example of Debug provider
output from the code shown above.
Log filtering
You can specify a minimum log level for a specific provider and category or for all providers or all categories.
Any logs below the minimum level aren't passed to that provider, so they don't get displayed or stored.
If you want to suppress all logs, you can specify LogLevel.None as the minimum log level. The integer value of
LogLevel.None is 6, which is higher than LogLevel.Critical (5 ).
webHost.Run();
}
The configuration data specifies minimum log levels by provider and category, as in the following example:
{
"Logging": {
"Debug": {
"LogLevel": {
"Default": "Information"
}
},
"Console": {
"IncludeScopes": false,
"LogLevel": {
"Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
"Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
"Microsoft.AspNetCore.Mvc.Razor": "Error",
"Default": "Information"
}
},
"LogLevel": {
"Default": "Debug"
}
}
}
This JSON creates six filter rules, one for the Debug provider, four for the Console provider, and one that applies
to all providers. You'll see later how just one of these rules is chosen for each provider when an ILogger object
is created.
Filter rules in code
You can register filter rules in code, as shown in the following example:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
logging.AddFilter("System", LogLevel.Debug)
.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Trace))
.Build();
The second AddFilter specifies the Debug provider by using its type name. The first AddFilter applies to all
providers because it doesn't specify a provider type.
How filtering rules are applied
The configuration data and the AddFilter code shown in the preceding examples create the rules shown in the
following table. The first six come from the configuration example and the last two come from the code example.
When you create an ILogger object to write logs with, the ILoggerFactory object selects a single rule per
provider to apply to that logger. All messages written by that ILogger object are filtered based on the selected
rules. The most specific rule possible for each provider and category pair is selected from the available rules.
The following algorithm is used for each provider when an ILogger is created for a given category:
Select all rules that match the provider or its alias. If none are found, select all rules with an empty provider.
From the result of the preceding step, select rules with longest matching category prefix. If none are found,
select all rules that don't specify a category.
If multiple rules are selected take the last one.
If no rules are selected, use MinimumLevel .
For example, suppose you have the preceding list of rules and you create an ILogger object for category
"Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine":
For the Debug provider, rules 1, 6, and 8 apply. Rule 8 is most specific, so that's the one selected.
For the Console provider, rules 3, 4, 5, and 6 apply. Rule 3 is most specific.
When you create logs with an ILogger for category "Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine", logs
of Trace level and above will go to the Debug provider, and logs of Debug level and above will go to the
Console provider.
Provider aliases
You can use the type name to specify a provider in configuration, but each provider defines a shorter alias that's
easier to use. For the built-in providers, use the following aliases:
Console
Debug
EventLog
AzureAppServices
TraceSource
EventSource
Default minimum level
There's a minimum level setting that takes effect only if no rules from configuration or code apply for a given
provider and category. The following example shows how to set the minimum level:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
.Build();
If you don't explicitly set the minimum level, the default value is Information , which means that Trace and
Debug logs are ignored.
Filter functions
You can write code in a filter function to apply filtering rules. A filter function is invoked for all providers and
categories that don't have rules assigned to them by configuration or code. Code in the function has access to
the provider type, category, and log level to decide whether or not a message should be logged. For example:
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logBuilder =>
{
logBuilder.AddFilter((provider, category, logLevel) =>
{
if (provider == "Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider" &&
category == "TodoApiSample.Controllers.TodoController")
{
return false;
}
return true;
});
})
.Build();
Some logging providers let you specify when logs should be written to a storage medium or ignored based on
log level and category.
The AddConsole and AddDebug extension methods provide overloads that let you pass in filtering criteria. The
following sample code causes the console provider to ignore logs below Warning level, while the Debug
provider ignores logs that the framework creates.
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole(LogLevel.Warning)
.AddDebug((category, logLevel) => (category.Contains("TodoApi") && logLevel >= LogLevel.Trace));
The AddEventLog method has an overload that takes an EventLogSettings instance, which may contain a
filtering function in its Filter property. The TraceSource provider doesn't provide any of those overloads, since
its logging level and other parameters are based on the SourceSwitch and TraceListener it uses.
You can set filtering rules for all providers that are registered with an ILoggerFactory instance by using the
WithFilter extension method. The example below limits framework logs (category begins with "Microsoft" or
"System") to warnings while letting the app log at debug level.
If you want to use filtering to prevent all logs from being written for a particular category, you can specify
LogLevel.None as the minimum log level for that category. The integer value of LogLevel.None is 6, which is
higher than LogLevel.Critical (5).
The WithFilter extension method is provided by the Microsoft.Extensions.Logging.Filter NuGet package. The
method returns a new ILoggerFactory instance that will filter the log messages passed to all logger providers
registered with it. It doesn't affect any other ILoggerFactory instances, including the original ILoggerFactory
instance.
Log scopes
You can group a set of logical operations within a scope in order to attach the same data to each log that's
created as part of that set. For example, you might want every log created as part of processing a transaction to
include the transaction ID.
A scope is an IDisposable type that's returned by the ILogger.BeginScope<TState> method and lasts until it's
disposed. You use a scope by wrapping your logger calls in a using block, as shown here:
public IActionResult GetById(string id)
{
TodoItem item;
using (_logger.BeginScope("Message attached to logs created in the using block"))
{
_logger.LogInformation(LoggingEvents.GetItem, "Getting item {ID}", id);
item = _todoRepository.Find(id);
if (item == null)
{
_logger.LogWarning(LoggingEvents.GetItemNotFound, "GetById({ID}) NOT FOUND", id);
return NotFound();
}
}
return new ObjectResult(item);
}
NOTE
Configuring the IncludeScopes console logger option is required to enable scope-based logging.
For information on configuration, see the Configuration section.
Program.cs:
NOTE
Configuring the IncludeScopes console logger option is required to enable scope-based logging.
Startup.cs:
logging.AddConsole();
loggerFactory.AddConsole();
AddConsole overloads let you pass in an a minimum log level, a filter function, and a boolean that indicates
whether scopes are supported. Another option is to pass in an IConfiguration object, which can specify scopes
support and logging levels.
If you are considering the console provider for use in production, be aware that it has a significant impact on
performance.
When you create a new project in Visual Studio, the AddConsole method looks like this:
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
{
"Logging": {
"Console": {
"IncludeScopes": false
},
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
The settings shown limit framework logs to warnings while allowing the app to log at debug level, as explained
in the Log filtering section. For more information, see Configuration.
Debug provider
The Microsoft.Extensions.Logging.Debug provider package writes log output by using the
System.Diagnostics.Debug class ( Debug.WriteLine method calls).
On Linux, this provider writes logs to /var/log/message.
logging.AddDebug();
loggerFactory.AddDebug();
AddDebug overloads let you pass in a minimum log level or a filter function.
EventSource provider
For apps that target ASP.NET Core 1.1.0 or later, the Microsoft.Extensions.Logging.EventSource provider
package can implement event tracing. On Windows, it uses ETW. The provider is cross-platform, but there are
no event collection and display tools yet for Linux or macOS.
logging.AddEventSourceLogger();
loggerFactory.AddEventSourceLogger();
A good way to collect and view logs is to use the PerfView utility. There are other tools for viewing ETW logs,
but PerfView provides the best experience for working with the ETW events emitted by ASP.NET.
To configure PerfView for collecting events logged by this provider, add the string
*Microsoft-Extensions-Logging to the Additional Providers list. ( Don't miss the asterisk at the start of the
string.)
loggerFactory.AddEventLog();
logging.AddTraceSource(sourceSwitchName);
loggerFactory.AddTraceSource(sourceSwitchName);
AddTraceSource overloads let you pass in a source switch and a trace listener.
To use this provider, an application has to run on the .NET Framework (rather than .NET Core). The provider lets
you route messages to a variety of listeners, such as the TextWriterTraceListener used in the sample application.
The following example configures a TraceSource provider that logs Warning and higher messages to the
console window.
loggerFactory.AddAzureWebAppDiagnostics();
When you deploy to an App Service app, the app honors the settings in the Diagnostic Logs section of the App
Service page of the Azure portal. When these settings are updated, the changes take effect immediately without
requiring a restart or redeployment of the app.
The default location for log files is in the D:\home\LogFiles\Application folder, and the default file name is
diagnostics-yyyymmdd.txt. The default file size limit is 10 MB, and the default maximum number of files retained
is 2. The default blob name is {app -name}{timestamp }/yyyy/mm/dd/hh/{guid }-applicationLog.txt. For more
information about default behavior, see AzureAppServicesDiagnosticsSettings.
The provider only works when the project runs in the Azure environment. It has no effect when the project is run
locally—it doesn't write to local files or local development storage for blobs.
Additional resources
High-performance logging with LoggerMessage in ASP.NET Core
High-performance logging with LoggerMessage in
ASP.NET Core
6/21/2018 • 7 minutes to read • Edit Online
By Luke Latham
LoggerMessage features create cacheable delegates that require fewer object allocations and reduced
computational overhead compared to logger extension methods, such as LogInformation , LogDebug , and
LogError . For high-performance logging scenarios, use the LoggerMessage pattern.
LoggerMessage provides the following performance advantages over Logger extension methods:
Logger extension methods require "boxing" (converting) value types, such as int , into object . The
LoggerMessage pattern avoids boxing by using static Action fields and extension methods with strongly-typed
parameters.
Logger extension methods must parse the message template (named format string) every time a log message
is written. LoggerMessage only requires parsing a template once when the message is defined.
View or download sample code (how to download)
The sample app demonstrates LoggerMessage features with a basic quote tracking system. The app adds and
deletes quotes using an in-memory database. As these operations occur, log messages are generated using the
LoggerMessage pattern.
LoggerMessage.Define
Define(LogLevel, EventId, String) creates an Action delegate for logging a message. Define overloads permit
passing up to six type parameters to a named format string (template).
The string provided to the Define method is a template and not an interpolated string. Placeholders are filled in
the order that the types are specified. Placeholder names in the template should be descriptive and consistent
across templates. They serve as property names within structured log data. We recommend Pascal casing for
placeholder names. For example, {Count} , {FirstName} .
Each log message is an Action held in a static field created by LoggerMessage.Define . For example, the sample app
creates a field to describe a log message for a GET request for the Index page (Internal/LoggerExtensions.cs):
Structured logging stores may use the event name when it's supplied with the event id to enrich logging. For
example, Serilog uses the event name.
The Action is invoked through a strongly-typed extension method. The IndexPageRequested method logs a
message for an Index page GET request in the sample app:
info: LoggerMessageSample.Pages.IndexModel[1]
=> RequestId:0HL90M6E7PHK4:00000001 RequestPath:/ => /Index
GET request for Index page
To pass parameters to a log message, define up to six types when creating the static field. The sample app logs a
string when adding a quote by defining a string type for the Action field:
The delegate's log message template receives its placeholder values from the types provided. The sample app
defines a delegate for adding a quote where the quote parameter is a string :
_quoteAdded = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(2, nameof(QuoteAdded)),
"Quote added (Quote = '{Quote}')");
The static extension method for adding a quote, QuoteAdded , receives the quote argument value and passes it to
the Action delegate:
In the Index page's page model (Pages/Index.cshtml.cs), QuoteAdded is called to log the message:
public async Task<IActionResult> OnPostAddQuoteAsync()
{
_db.Quotes.Add(Quote);
await _db.SaveChangesAsync();
_logger.QuoteAdded(Quote.Text);
return RedirectToPage();
}
info: LoggerMessageSample.Pages.IndexModel[2]
=> RequestId:0HL90M6E7PHK5:0000000A RequestPath:/ => /Index
Quote added (Quote = 'You can avoid reality, but you cannot avoid the consequences of avoiding reality.
- Ayn Rand')
The sample app implements a try – catch pattern for quote deletion. An informational message is logged for a
successful delete operation. An error message is logged for a delete operation when an exception is thrown. The
log message for the unsuccessful delete operation includes the exception stack trace (Internal/LoggerExtensions.cs):
_quoteDeleteFailed = LoggerMessage.Define<int>(
LogLevel.Error,
new EventId(5, nameof(QuoteDeleteFailed)),
"Quote delete failed (Id = {Id})");
public static void QuoteDeleted(this ILogger logger, string quote, int id)
{
_quoteDeleted(logger, quote, id, null);
}
public static void QuoteDeleteFailed(this ILogger logger, int id, Exception ex)
{
_quoteDeleteFailed(logger, id, ex);
}
In the page model for the Index page, a successful quote deletion calls the QuoteDeleted method on the logger.
When a quote isn't found for deletion, an ArgumentNullException is thrown. The exception is trapped by the try –
catch statement and logged by calling the QuoteDeleteFailed method on the logger in the catch block
(Pages/Index.cshtml.cs):
public async Task<IActionResult> OnPostDeleteQuoteAsync(int id)
{
var quote = await _db.Quotes.FindAsync(id);
_logger.QuoteDeleted(quote.Text, id);
}
catch (ArgumentNullException ex)
{
_logger.QuoteDeleteFailed(id, ex);
}
return RedirectToPage();
}
info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:00000016 RequestPath:/ => /Index
Quote deleted (Quote = 'You can avoid reality, but you cannot avoid the consequences of avoiding
reality. - Ayn Rand' Id = 1)
When quote deletion fails, inspect the app's console output. Note that the exception is included in the log message:
fail: LoggerMessageSample.Pages.IndexModel[5]
=> RequestId:0HL90M6E7PHK5:00000010 RequestPath:/ => /Index
Quote delete failed (Id = 999)
System.ArgumentNullException: Value cannot be null.
Parameter name: entity
at Microsoft.EntityFrameworkCore.Utilities.Check.NotNull[T](T value, String parameterName)
at Microsoft.EntityFrameworkCore.DbContext.Remove[TEntity](TEntity entity)
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Remove(TEntity entity)
at LoggerMessageSample.Pages.IndexModel.<OnPostDeleteQuoteAsync>d__14.MoveNext() in
<PATH>\sample\Pages\Index.cshtml.cs:line 87
LoggerMessage.DefineScope
DefineScope(String) creates a Func delegate for defining a log scope. DefineScope overloads permit passing up to
three type parameters to a named format string (template).
As is the case with the Define method, the string provided to the DefineScope method is a template and not an
interpolated string. Placeholders are filled in the order that the types are specified. Placeholder names in the
template should be descriptive and consistent across templates. They serve as property names within structured
log data. We recommend Pascal casing for placeholder names. For example, {Count} , {FirstName} .
Define a log scope to apply to a series of log messages using the DefineScope(String) method.
The sample app has a Clear All button for deleting all of the quotes in the database. The quotes are deleted by
removing them one at a time. Each time a quote is deleted, the QuoteDeleted method is called on the logger. A log
scope is added to these log messages.
Enable IncludeScopes in the console logger section of appsettings.json:
{
"Logging": {
"Console": {
"IncludeScopes": true
},
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
To create a log scope, add a field to hold a Func delegate for the scope. The sample app creates a field called
_allQuotesDeletedScope ( Internal/LoggerExtensions.cs):
Use DefineScope to create the delegate. Up to three types can be specified for use as template arguments when
the delegate is invoked. The sample app uses a message template that includes the number of deleted quotes (an
int type):
Provide a static extension method for the log message. Include any type parameters for named properties that
appear in the message template. The sample app takes in a count of quotes to delete and returns
_allQuotesDeletedScope :
using (_logger.AllQuotesDeletedScope(quoteCount))
{
foreach (Quote quote in _db.Quotes)
{
_db.Quotes.Remove(quote);
_logger.QuoteDeleted(quote.Text, quote.Id);
}
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
Inspect the log messages in the app's console output. The following result shows three quotes deleted with the log
scope message included:
info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:0000002E RequestPath:/ => /Index => All quotes deleted (Count = 3)
Quote deleted (Quote = 'Quote 1' Id = 2)
info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:0000002E RequestPath:/ => /Index => All quotes deleted (Count = 3)
Quote deleted (Quote = 'Quote 2' Id = 3)
info: LoggerMessageSample.Pages.IndexModel[4]
=> RequestId:0HL90M6E7PHK5:0000002E RequestPath:/ => /Index => All quotes deleted (Count = 3)
Quote deleted (Quote = 'Quote 3' Id = 4)
Additional resources
Logging
Handle errors in ASP.NET Core
9/6/2018 • 7 minutes to read • Edit Online
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
Place the call to UseDeveloperExceptionPage in front of any middleware where you want to catch exceptions,
such as app.UseMvc .
WARNING
Enable the Developer Exception Page only when the app is running in the Development environment. You don't want
to share detailed exception information publicly when the app runs in production. Learn more about configuring
environments.
To see the Developer Exception Page, run the sample app with the environment set to Development and add
?throw=true to the base URL of the app. The page includes several tabs with information about the exception
and the request. The first tab includes a stack trace:
The next tab shows the query string parameters, if any:
If the request has cookies, they appear on the Cookies tab. Headers are seen in the last tab:
Configure a custom exception handling page
Configure an exception handler page to use when the app isn't running in the Development environment:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
In a Razor Pages app, the dotnet new Razor Pages template provides an Error page and an error PageModel class
in the Pages folder.
In an MVC app, don't decorate the error handler action method with HTTP method attributes, such as HttpGet .
Explicit verbs prevent some requests from reaching the method. Allow anonymous access to the method so that
unauthenticated users are able to receive the error view.
For example, the following error handler method is provided by the dotnet new MVC template and appears in
the Home controller:
[AllowAnonymous]
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
app.UseStatusCodePages();
UseStatusCodePages should be called before request handling middlewares in the pipeline (for example, Static
Files Middleware and MVC Middleware).
By default, Status Code Pages Middleware adds text-only handlers for common status codes, such as 404:
The middleware supports several extension methods. One method takes a lambda expression:
await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});
There are also redirect and re-execute extension methods. The redirect method sends a 302 Found status code to
the client and redirects the client to the provided location URL template. The template may include a {0}
placeholder for the status code. URLs starting with ~ have the base path prepended. A URL that doesn't start
with ~ is used as is.
app.UseStatusCodePagesWithRedirects("/error/{0}");
The re-execute method returns the original status code to the client and specifies that the response body should
be generated by re-executing the request pipeline using an alternate path. This path may contain a {0}
placeholder for the status code:
app.UseStatusCodePagesWithReExecute("/error/{0}");
Status code pages can be disabled for specific requests in a Razor Pages handler method or in an MVC controller.
To disable status code pages, attempt to retrieve the IStatusCodePagesFeature from the request's
HttpContext.Features collection and disable the feature if it's available:
if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}
If using a UseStatusCodePages* overload that points to an endpoint within the app, create an MVC view or Razor
Page for the endpoint. For example, the dotnet new template for a Razor Pages app produces the following page
and page model class:
Error.cshtml:
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed
information about the error that occurred.
</p>
<p>
<strong>Development environment should not be enabled in deployed applications
</strong>, as it can result in sensitive information from exceptions being
displayed to end users. For local debugging, development environment can be
enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment
variable to <strong>Development</strong>, and restarting the application.
</p>
Error.cshtml.cs:
Exception-handling code
Code in exception handling pages can throw exceptions. It's often a good idea for production error pages to
consist of purely static content.
Also, be aware that once the headers for a response have been sent, you can't change the response's status code,
nor can any exception pages or handlers run. The response must be completed or the connection aborted.
Hosting can only show an error page for a captured startup error if the error occurs after host address/port
binding. If any binding fails for any reason, the hosting layer logs a critical exception, the dotnet process crashes,
and no error page is displayed when the app is running on the Kestrel server.
When running on IIS or IIS Express, a 502.5 Process Failure is returned by the ASP.NET Core Module if the
process can't be started. For information on troubleshooting startup issues when hosting with IIS, see
Troubleshoot ASP.NET Core on IIS. For information on troubleshooting startup issues with Azure App Service,
see Troubleshoot ASP.NET Core on Azure App Service.
TIP
Exception filters are good for trapping exceptions that occur within MVC actions, but they're not as flexible as error
handling middleware. Generally prefer the use of middleware, and use filters only where you need to perform error
handling differently based on which MVC action is chosen.
Additional resources
Common errors reference for Azure App Service and IIS with ASP.NET Core
Troubleshoot ASP.NET Core on IIS
Troubleshoot ASP.NET Core on Azure App Service
Host in ASP.NET Core
8/31/2018 • 2 minutes to read • Edit Online
.NET apps configure and launch a host. The host is responsible for app startup and lifetime management. Two
host APIs are available for use:
Web Host – Suitable for hosting web apps.
Generic Host (ASP.NET Core 2.1 or later) – Suitable for hosting non-web apps (for example, apps that run
background tasks). In a future release, the Generic Host will be suitable for hosting any kind of app,
including web apps. The Generic Host will eventually replace the Web Host.
For hosting ASP.NET Core web apps, developers should use the Web Host based on IWebHostBuilder. For
hosting non-web apps, developers should use the Generic Host based on HostBuilder.
Background tasks with hosted services in ASP.NET Core
Learn how to implement background tasks with hosted services in ASP.NET Core.
Enhance an app from an external assembly in ASP.NET Core with IHostingStartup
Discover how to enhance an ASP.NET Core app from a referenced or unreferenced assembly using an
IHostingStartup implementation.
ASP.NET Core Web Host
9/7/2018 • 17 minutes to read • Edit Online
By Luke Latham
ASP.NET Core apps configure and launch a host. The host is responsible for app startup and lifetime
management. At a minimum, the host configures a server and a request processing pipeline. This topic covers the
ASP.NET Core Web Host (IWebHostBuilder), which is useful for hosting web apps. For coverage of the .NET
Generic Host (IHostBuilder), see .NET Generic Host.
Set up a host
Create a host using an instance of IWebHostBuilder. This is typically performed in the app's entry point, the Main
method. In the project templates, Main is located in Program.cs. A typical Program.cs calls CreateDefaultBuilder
to start setting up a host:
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true);
})
...
The following ConfigureLogging call adds a delegate to configure the minimum logging level
(SetMinimumLevel) to LogLevel.Warning. This setting overrides the settings in
appsettings.Development.json ( LogLevel.Debug ) and appsettings.Production.json ( LogLevel.Error )
configured by CreateDefaultBuilder . ConfigureLogging may be called multiple times.
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Warning);
})
...
WebHost.CreateDefaultBuilder(args)
.ConfigureKestrel((context, options) =>
{
options.Limits.MaxRequestBodySize = 20000000;
});
...
The following call to UseKestrel overrides the default Limits.MaxRequestBodySize of 30,000,000 bytes
established when Kestrel was configured by CreateDefaultBuilder :
WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
{
options.Limits.MaxRequestBodySize = 20000000;
});
...
The content root determines where the host searches for content files, such as MVC view files. When the app is
started from the project's root folder, the project's root folder is used as the content root. This is the default used
in Visual Studio and the dotnet new templates.
For more information on app configuration, see Configuration in ASP.NET Core.
NOTE
As an alternative to using the static CreateDefaultBuilder method, creating a host from WebHostBuilder is a supported
approach with ASP.NET Core 2.x. For more information, see the ASP.NET Core 1.x tab.
Create a host using an instance of WebHostBuilder. Creating a host is typically performed in the app's entry point,
the Main method. In the project templates, Main is located in Program.cs:
WebHostBuilder requires a server that implements IServer. The built-in servers are Kestrel and HTTP.sys (prior to
the release of ASP.NET Core 2.0, HTTP.sys was called WebListener). In this example, the UseKestrel extension
method specifies the Kestrel server.
The content root determines where the host searches for content files, such as MVC view files. The default content
root is obtained for UseContentRoot by Directory.GetCurrentDirectory. When the app is started from the project's
root folder, the project's root folder is used as the content root. This is the default used in Visual Studio and the
dotnet new templates.
To use IIS as a reverse proxy, call UseIISIntegration as part of building the host. UseIISIntegration doesn't
configure a server, like UseKestrel does. UseIISIntegration configures the base path and port the server listens
on when using the ASP.NET Core Module to create a reverse proxy between Kestrel and IIS. To use IIS with
ASP.NET Core, UseKestrel and UseIISIntegration must be specified. UseIISIntegration only activates when
running behind IIS or IIS Express. For more information, see ASP.NET Core Module and ASP.NET Core Module
configuration reference.
A minimal implementation that configures a host (and an ASP.NET Core app) includes specifying a server and
configuration of the app's request pipeline:
host.Run();
When setting up a host, Configure and ConfigureServices methods can be provided. If a Startup class is
specified, it must define a Configure method. For more information, see Application startup in ASP.NET Core.
Multiple calls to ConfigureServices append to one another. Multiple calls to Configure or UseStartup on the
WebHostBuilder replace previous settings.
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.ApplicationKey, "CustomApplicationName")
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
Content Root
This setting determines where ASP.NET Core begins searching for content files, such as MVC views.
Key: contentRoot
Type: string
Default: Defaults to the folder where the app assembly resides.
Set using: UseContentRoot
Environment variable: ASPNETCORE_CONTENTROOT
The content root is also used as the base path for the Web Root setting. If the path doesn't exist, the host fails to
start.
WebHost.CreateDefaultBuilder(args)
.UseContentRoot("c:\\<content-root>")
Detailed Errors
Determines if detailed errors should be captured.
Key: detailedErrors
Type: bool ( true or 1 )
Default: false
Set using: UseSetting
Environment variable: ASPNETCORE_DETAILEDERRORS
When enabled (or when the Environment is set to Development ), the app captures detailed exceptions.
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
Environment
Sets the app's environment.
Key: environment
Type: string
Default: Production
Set using: UseEnvironment
Environment variable: ASPNETCORE_ENVIRONMENT
The environment can be set to any value. Framework-defined values include Development , Staging , and
Production . Values aren't case sensitive. By default, the Environment is read from the ASPNETCORE_ENVIRONMENT
environment variable. When using Visual Studio, environment variables may be set in the launchSettings.json file.
For more information, see Use multiple environments in ASP.NET Core.
WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2")
HTTPS Port
Set the HTTPS redirect port. Used in enforcing HTTPS.
Key: https_port Type: string Default: A default value isn't set. Set using: UseSetting Environment variable:
ASPNETCORE_HTTPS_PORT
WebHost.CreateDefaultBuilder(args)
.UseSetting("https_port", "8080")
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, "assembly1;assembly2")
WebHost.CreateDefaultBuilder(args)
.PreferHostingUrls(false)
WebHost.CreateDefaultBuilder(args)
.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true")
Server URLs
Indicates the IP addresses or host addresses with ports and protocols that the server should listen on for
requests.
Key: urls
Type: string
Default: http://localhost:5000
Set using: UseUrls
Environment variable: ASPNETCORE_URLS
Set to a semicolon-separated (;) list of URL prefixes to which the server should respond. For example,
http://localhost:123 . Use "*" to indicate that the server should listen for requests on any IP address or
hostname using the specified port and protocol (for example, http://*:5000 ). The protocol ( http:// or
https:// ) must be included with each URL. Supported formats vary between servers.
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")
Kestrel has its own endpoint configuration API. For more information, see Kestrel web server implementation in
ASP.NET Core.
Shutdown Timeout
Specifies the amount of time to wait for the web host to shut down.
Key: shutdownTimeoutSeconds
Type: int
Default: 5
Set using: UseShutdownTimeout
Environment variable: ASPNETCORE_SHUTDOWNTIMEOUTSECONDS
Although the key accepts an int with UseSetting(for example,
.UseSetting(WebHostDefaults.ShutdownTimeoutKey, "10") ), the UseShutdownTimeout extension method takes a
TimeSpan.
During the timeout period, hosting:
Triggers IApplicationLifetime.ApplicationStopping.
Attempts to stop hosted services, logging any errors for services that fail to stop.
If the timeout period expires before all of the hosted services stop, any remaining active services are stopped
when the app shuts down. The services stop even if they haven't finished processing. If services require additional
time to stop, increase the timeout.
WebHost.CreateDefaultBuilder(args)
.UseShutdownTimeout(TimeSpan.FromSeconds(10))
Startup Assembly
Determines the assembly to search for the Startup class.
Key: startupAssembly
Type: string
Default: The app's assembly
Set using: UseStartup
Environment variable: ASPNETCORE_STARTUPASSEMBLY
The assembly by name ( string ) or type ( TStartup ) can be referenced. If multiple UseStartup methods are
called, the last one takes precedence.
WebHost.CreateDefaultBuilder(args)
.UseStartup("StartupAssemblyName")
WebHost.CreateDefaultBuilder(args)
.UseStartup<TStartup>()
Web Root
Sets the relative path to the app's static assets.
Key: webroot
Type: string
Default: If not specified, the default is "(Content Root)/wwwroot", if the path exists. If the path doesn't exist, then
a no-op file provider is used.
Set using: UseWebRoot
Environment variable: ASPNETCORE_WEBROOT
WebHost.CreateDefaultBuilder(args)
.UseWebRoot("public")
return WebHost.CreateDefaultBuilder(args)
.UseUrls("http://*:5000")
.UseConfiguration(config)
.Configure(app =>
{
app.Run(context =>
context.Response.WriteAsync("Hello, World!"));
})
.Build();
}
}
hostsettings.json:
{
urls: "http://*:5005"
}
Overriding the configuration provided by UseUrls with hostsettings.json config first, command-line argument
config second:
public class Program
{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("hostsettings.json", optional: true)
.AddCommandLine(args)
.Build();
host.Run();
}
}
hostsettings.json:
{
urls: "http://*:5005"
}
NOTE
The UseConfiguration extension method isn't currently capable of parsing a configuration section returned by GetSection
(for example, .UseConfiguration(Configuration.GetSection("section")) . The GetSection method filters the
configuration keys to the section requested but leaves the section name on the keys (for example, section:urls ,
section:environment ). The UseConfiguration method expects the keys to match the WebHostBuilder keys (for
example, urls , environment ). The presence of the section name on the keys prevents the section's values from
configuring the host. This issue will be addressed in an upcoming release. For more information and workarounds, see
Passing configuration section into WebHostBuilder.UseConfiguration uses full keys.
UseConfiguration only copies keys from the provided IConfiguration to the host builder configuration. Therefore,
setting reloadOnChange: true for JSON, INI, and XML settings files has no effect.
To specify the host run on a particular URL, the desired value can be passed in from a command prompt when
executing dotnet run. The command-line argument overrides the urls value from the hostsettings.json file, and
the server listens on port 8080:
Start
Run the host in a non-blocking manner by calling its Start method:
using (host)
{
host.Start();
Console.ReadLine();
}
If a list of URLs is passed to the Start method, it listens on the URLs specified:
using (host)
{
Console.ReadLine();
}
The app can initialize and start a new host using the pre-configured defaults of CreateDefaultBuilder using a
static convenience method. These methods start the server without console output and with WaitForShutdown
wait for a break (Ctrl-C/SIGINT or SIGTERM ):
Start(RequestDelegate app)
Start with a RequestDelegate :
Make a request in the browser to http://localhost:5000 to receive the response "Hello World!" WaitForShutdown
blocks until a break (Ctrl-C/SIGINT or SIGTERM ) is issued. The app displays the Console.WriteLine message and
waits for a keypress to exit.
Start(string url, RequestDelegate app)
Start with a URL and RequestDelegate :
REQUEST RESPONSE
WaitForShutdown blocks until a break (Ctrl-C/SIGINT or SIGTERM ) is issued. The app displays the
Console.WriteLine message and waits for a keypress to exit.
StartWith(Action<IApplicationBuilder> app)
Provide a delegate to configure an IApplicationBuilder :
Make a request in the browser to http://localhost:5000 to receive the response "Hello World!" WaitForShutdown
blocks until a break (Ctrl-C/SIGINT or SIGTERM ) is issued. The app displays the Console.WriteLine message and
waits for a keypress to exit.
StartWith(string url, Action<IApplicationBuilder> app)
Provide a URL and a delegate to configure an IApplicationBuilder :
Produces the same result as StartWith(Action<IApplicationBuilder> app), except the app responds on
http://localhost:8080 .
Run
The Run method starts the web app and blocks the calling thread until the host is shut down:
host.Run();
Start
Run the host in a non-blocking manner by calling its Start method:
using (host)
{
host.Start();
Console.ReadLine();
}
If a list of URLs is passed to the Start method, it listens on the URLs specified:
using (host)
{
Console.ReadLine();
}
IHostingEnvironment interface
The IHostingEnvironment interface provides information about the app's web hosting environment. Use
constructor injection to obtain the IHostingEnvironment in order to use its properties and extension methods:
A convention-based approach can be used to configure the app at startup based on the environment.
Alternatively, inject the IHostingEnvironment into the Startup constructor for use in ConfigureServices :
public class Startup
{
public Startup(IHostingEnvironment env)
{
HostingEnvironment = env;
}
NOTE
In addition to the IsDevelopment extension method, IHostingEnvironment offers IsStaging , IsProduction , and
IsEnvironment(string environmentName) methods. For more information, see Use multiple environments in ASP.NET
Core.
The IHostingEnvironment service can also be injected directly into the Configure method for setting up the
processing pipeline:
IHostingEnvironment can be injected into the Invoke method when creating custom middleware:
public async Task Invoke(HttpContext context, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
// Configure middleware for Development
}
else
{
// Configure middleware for Staging/Production
}
IApplicationLifetime interface
IApplicationLifetime allows for post-startup and shutdown activities. Three properties on the interface are
cancellation tokens used to register Action methods that define startup and shutdown events.
StopApplication requests termination of the app. The following class uses StopApplication to gracefully shut
down an app when the class's Shutdown method is called:
Scope validation
CreateDefaultBuilder sets ServiceProviderOptions.ValidateScopes to true if the app's environment is
Development.
When ValidateScopes is set to true , the default service provider performs checks to verify that:
Scoped services aren't directly or indirectly resolved from the root service provider.
Scoped services aren't directly or indirectly injected into singletons.
The root service provider is created when BuildServiceProvider is called. The root service provider's lifetime
corresponds to the app/server's lifetime when the provider starts with the app and is disposed when the app
shuts down.
Scoped services are disposed by the container that created them. If a scoped service is created in the root
container, the service's lifetime is effectively promoted to singleton because it's only disposed by the root
container when app/server is shut down. Validating service scopes catches these situations when
BuildServiceProvider is called.
To always validate scopes, including in the Production environment, configure the ServiceProviderOptions with
UseDefaultServiceProvider on the host builder:
WebHost.CreateDefaultBuilder(args)
.UseDefaultServiceProvider((context, options) => {
options.ValidateScopes = true;
})
Troubleshooting System.ArgumentException
The following only applies to ASP.NET Core 2.0 apps when the app doesn't call UseStartup or Configure
.
A host may be built by injecting IStartup directly into the dependency injection container rather than calling
UseStartup or Configure :
services.AddSingleton<IStartup, Startup>();
If the host is built this way, the following error may occur:
This occurs because the app name (the name of the current assembly) is required to scan for
HostingStartupAttributes . If the app manually injects IStartup into the dependency injection container, add the
following call to WebHostBuilder with the assembly name specified:
WebHost.CreateDefaultBuilder(args)
.UseSetting("applicationName", "AssemblyName")
Alternatively, add a dummy Configure to the WebHostBuilder , which sets the app name automatically:
WebHost.CreateDefaultBuilder(args)
.Configure(_ => { })
Additional resources
Host ASP.NET Core on Windows with IIS
Host ASP.NET Core on Linux with Nginx
Host ASP.NET Core on Linux with Apache
Host ASP.NET Core in a Windows Service
.NET Generic Host
9/6/2018 • 10 minutes to read • Edit Online
By Luke Latham
.NET Core apps configure and launch a host. The host is responsible for app startup and lifetime management.
This topic covers the ASP.NET Core Generic Host (HostBuilder), which is useful for hosting apps that don't
process HTTP requests. For coverage of the Web Host (WebHostBuilder), see ASP.NET Core Web Host.
The goal of the Generic Host is to decouple the HTTP pipeline from the Web Host API to enable a wider array of
host scenarios. Messaging, background tasks, and other non-HTTP workloads based on the Generic Host benefit
from cross-cutting capabilities, such as configuration, dependency injection (DI), and logging.
The Generic Host is new in ASP.NET Core 2.1 and isn't suitable for web hosting scenarios. For web hosting
scenarios, use the Web Host. The Generic Host is under development to replace the Web Host in a future release
and act as the primary host API in both HTTP and non-HTTP scenarios.
View or download sample code (how to download)
When running the sample app in Visual Studio Code, use an external or integrated terminal. Don't run the
sample in an internalConsole .
To set the console in Visual Studio Code:
1. Open the .vscode/launch.json file.
2. In the .NET Core Launch (console) configuration, locate the console entry. Set the value to either
externalTerminal or integratedTerminal .
Introduction
The Generic Host library is available in the Microsoft.Extensions.Hosting namespace and provided by the
Microsoft.Extensions.Hosting package. The Microsoft.Extensions.Hosting package is included in the
Microsoft.AspNetCore.App metapackage (ASP.NET Core 2.1 or later).
IHostedService is the entry point to code execution. Each IHostedService implementation is executed in the order
of service registration in ConfigureServices. StartAsync is called on each IHostedService when the host starts,
and StopAsync is called in reverse registration order when the host shuts down gracefully.
Set up a host
IHostBuilder is the main component that libraries and apps use to initialize, build, and run the host:
await host.RunAsync();
}
Host configuration
HostBuilder relies on the following approaches to set the host configuration values:
Configuration builder
Extension method configuration
Configuration builder
Host builder configuration is created by calling ConfigureHostConfiguration on the IHostBuilder implementation.
ConfigureHostConfiguration uses an IConfigurationBuilder to create an IConfiguration for the host. The
configuration builder initializes the IHostingEnvironment for use in the app's build process.
Environment variable configuration isn't added by default. Call AddEnvironmentVariables on the host builder to
configure the host from environment variables. AddEnvironmentVariables accepts an optional user-defined prefix.
The sample app uses a prefix of PREFIX_ . The prefix is removed when the environment variables are read. When
the sample app's host is configured, the environment variable value for PREFIX_ENVIRONMENT becomes the host
configuration value for the environment key.
During development when using Visual Studio or running an app with dotnet run , environment variables may
be set in the Properties/launchSettings.json file. In Visual Studio Code, environment variables may be set in the
.vscode/launch.json file during development. For more information, see Use multiple environments in ASP.NET
Core.
ConfigureHostConfiguration can be called multiple times with additive results. The host uses whichever option
sets a value last.
hostsettings.json:
{
"environment": "Development"
}
NOTE
The AddConfiguration extension method isn't currently capable of parsing a configuration section returned by GetSection
(for example, .AddConfiguration(Configuration.GetSection("section")) . The GetSection method filters the
configuration keys to the section requested but leaves the section name on the keys (for example, section:environment ).
The AddConfiguration method expects the keys to match the HostBuilder keys (for example, environment ). The
presence of the section name on the keys prevents the section's values from configuring the host. This issue will be
addressed in an upcoming release. For more information and workarounds, see Passing configuration section into
WebHostBuilder.UseConfiguration uses full keys.
Content Root
This setting determines where the host begins searching for content files.
Key: contentRoot
Type: string
Default: Defaults to the folder where the app assembly resides.
Set using: UseContentRoot
Environment variable: <PREFIX_>CONTENTROOT ( <PREFIX_> is optional and user-defined)
If the path doesn't exist, the host fails to start.
Environment
Sets the app's environment.
Key: environment
Type: string
Default: Production
Set using: UseEnvironment
Environment variable: <PREFIX_>ENVIRONMENT ( <PREFIX_> is optional and user-defined)
The environment can be set to any value. Framework-defined values include Development , Staging , and
Production . Values aren't case sensitive.
ConfigureAppConfiguration
App builder configuration is created by calling ConfigureAppConfiguration on the IHostBuilder implementation.
ConfigureAppConfiguration uses an IConfigurationBuilder to create an IConfiguration for the app.
ConfigureAppConfiguration can be called multiple times with additive results. The app uses whichever option sets
a value last. The configuration created by ConfigureAppConfiguration is available at
HostBuilderContext.Configuration for subsequent operations and in Services.
Example app configuration using ConfigureAppConfiguration :
var host = new HostBuilder()
.ConfigureAppConfiguration((hostContext, configApp) =>
{
configApp.SetBasePath(Directory.GetCurrentDirectory());
configApp.AddJsonFile("appsettings.json", optional: true);
configApp.AddJsonFile(
$"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json",
optional: true);
configApp.AddEnvironmentVariables(prefix: "PREFIX_");
configApp.AddCommandLine(args);
})
appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
appsettings.Development.json:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
appsettings.Production.json:
{
"Logging": {
"LogLevel": {
"Default": "Error",
"System": "Information",
"Microsoft": "Information"
}
}
}
NOTE
The AddConfiguration extension method isn't currently capable of parsing a configuration section returned by GetSection
(for example, .AddConfiguration(Configuration.GetSection("section")) . The GetSection method filters the
configuration keys to the section requested but leaves the section name on the keys (for example,
section:Logging:LogLevel:Default ). The AddConfiguration method expects an exact match to configuration keys (for
example, Logging:LogLevel:Default ). The presence of the section name on the keys prevents the section's values from
configuring the app. This issue will be addressed in an upcoming release. For more information and workarounds, see
Passing configuration section into WebHostBuilder.UseConfiguration uses full keys.
To move settings files to the output directory, specify the settings files as MSBuild project items in the project file.
The sample app moves its JSON app settings files and hostsettings.json with the following <Content:> item:
<ItemGroup>
<Content Include="**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
ConfigureServices
ConfigureServices adds services to the app's dependency injection container. ConfigureServices can be called
multiple times with additive results.
A hosted service is a class with background task logic that implements the IHostedService interface. For more
information, see Background tasks with hosted services in ASP.NET Core.
The sample app uses the AddHostedService extension method to add a service for lifetime events,
LifetimeEventsHostedService , and a timed background task, TimedHostedService , to the app:
ConfigureLogging
ConfigureLogging adds a delegate for configuring the provided ILoggingBuilder. ConfigureLogging may be called
multiple times with additive results.
UseConsoleLifetime
UseConsoleLifetime listens for Ctrl+C /SIGINT or SIGTERM and calls StopApplication to start the shutdown
process. UseConsoleLifetime unblocks extensions such as RunAsync and WaitForShutdownAsync.
ConsoleLifetime is pre-registered as the default lifetime implementation. The last lifetime registered is used.
Container configuration
To support plugging in other containers, the host can accept an IServiceProviderFactory. Providing a factory isn't
part of the DI container registration but is instead a host intrinsic used to create the concrete DI container.
UseServiceProviderFactory(IServiceProviderFactory<TContainerBuilder>) overrides the default factory used to
create the app's service provider.
Custom container configuration is managed by the ConfigureContainer method. ConfigureContainer provides a
strongly-typed experience for configuring the container on top of the underlying host API. ConfigureContainer
can be called multiple times with additive results.
Create a service container for the app:
namespace GenericHostSample
{
internal class ServiceContainer
{
}
}
using System;
using Microsoft.Extensions.DependencyInjection;
namespace GenericHostSample
{
internal class ServiceContainerFactory : IServiceProviderFactory<ServiceContainer>
{
public ServiceContainer CreateBuilder(IServiceCollection services)
{
return new ServiceContainer();
}
Use the factory and configure the custom service container for the app:
Extensibility
Host extensibility is performed with extension methods on IHostBuilder . The following example shows how an
extension method extends an IHostBuilder implementation with the TimedHostedService example demonstrated
in Background tasks with hosted services in ASP.NET Core.
await host.StartAsync();
An app establishes the UseHostedService extension method to register the hosted service passed in T :
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
host.Run();
}
}
RunAsync
RunAsync runs the app and returns a Task that completes when the cancellation token or shutdown is triggered:
await host.RunAsync();
}
}
RunConsoleAsync
RunConsoleAsync enables console support, builds and starts the host, and waits for Ctrl+C /SIGINT or
SIGTERM to shut down.
public class Program
{
public static async Task Main(string[] args)
{
var hostBuilder = new HostBuilder();
await hostBuilder.RunConsoleAsync();
}
}
using (host)
{
host.Start();
await host.StopAsync(TimeSpan.FromSeconds(5));
}
}
}
using (host)
{
await host.StartAsync();
await host.StopAsync();
}
}
}
WaitForShutdown
WaitForShutdown is triggered via the IHostLifetime, such as ConsoleLifetime (listens for Ctrl+C /SIGINT or
SIGTERM ). WaitForShutdown calls StopAsync.
public class Program
{
public void Main(string[] args)
{
var host = new HostBuilder()
.Build();
using (host)
{
host.Start();
host.WaitForShutdown();
}
}
}
WaitForShutdownAsync
WaitForShutdownAsync returns a Task that completes when shutdown is triggered via the given token and calls
StopAsync.
using (host)
{
await host.StartAsync();
await host.WaitForShutdownAsync();
}
}
}
External control
External control of the host can be achieved using methods that can be called externally:
public class Program
{
private IHost _host;
public Program()
{
_host = new HostBuilder()
.Build();
}
IHostLifetime.WaitForStartAsync is called at the start of StartAsync, which waits until it's complete before
continuing. This can be used to delay startup until signaled by an external event.
IHostingEnvironment interface
IHostingEnvironment provides information about the app's hosting environment. Use constructor injection to
obtain the IHostingEnvironment in order to use its properties and extension methods:
IApplicationLifetime interface
IApplicationLifetime allows for post-startup and shutdown activities, including graceful shutdown requests. Three
properties on the interface are cancellation tokens used to register Action methods that define startup and
shutdown events.
Constructor-inject the IApplicationLifetime service into any class. The sample app uses constructor injection into
a LifetimeEventsHostedService class (an IHostedService implementation) to register the events.
LifetimeEventsHostedService.cs:
public LifetimeEventsHostedService(
ILogger<LifetimeEventsHostedService> logger, IApplicationLifetime appLifetime)
{
_logger = logger;
_appLifetime = appLifetime;
}
return Task.CompletedTask;
}
Additional resources
Background tasks with hosted services in ASP.NET Core
Hosting repo samples on GitHub
Background tasks with hosted services in ASP.NET
Core
9/26/2018 • 4 minutes to read • Edit Online
By Luke Latham
In ASP.NET Core, background tasks can be implemented as hosted services. A hosted service is a class with
background task logic that implements the IHostedService interface. This topic provides three hosted service
examples:
Background task that runs on a timer.
Hosted service that activates a scoped service. The scoped service can use dependency injection.
Queued background tasks that run sequentially.
View or download sample code (how to download)
The sample app is provided in two versions:
Web Host – The Web Host is useful for hosting web apps. The example code shown in this topic is from the
Web Host version of the sample. For more information, see the Web Host topic.
Generic Host – The Generic Host is new in ASP.NET Core 2.1. For more information, see the Generic Host
topic.
Package
Reference the Microsoft.AspNetCore.App metapackage or add a package reference to the
Microsoft.Extensions.Hosting package.
IHostedService interface
Hosted services implement the IHostedService interface. The interface defines two methods for objects that are
managed by the host:
StartAsync(CancellationToken) - StartAsync contains the logic to start the background task. When using
the Web Host, StartAsync is called after the server has started and IApplicationLifetime.ApplicationStarted
is triggered. When using the Generic Host, StartAsync is called before ApplicationStarted is triggered.
StopAsync(CancellationToken) - Triggered when the host is performing a graceful shutdown. StopAsync
contains the logic to end the background task and dispose of any unmanaged resources. If the app shuts
down unexpectedly (for example, the app's process fails), StopAsync might not be called.
The hosted service is activated once at app startup and gracefully shutdown at app shutdown. When IDisposable
is implemented, resources can be disposed when the service container is disposed. If an error is thrown during
background task execution, Dispose should be called even if StopAsync isn't called.
return Task.CompletedTask;
}
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
services.AddHostedService<TimedHostedService>();
The hosted service creates a scope to resolve the scoped background task service to call its DoWork method:
internal class ConsumeScopedServiceHostedService : IHostedService
{
private readonly ILogger _logger;
DoWork();
return Task.CompletedTask;
}
scopedProcessingService.DoWork();
}
}
return Task.CompletedTask;
}
}
The services are registered in Startup.ConfigureServices . The IHostedService implementation is registered with
the AddHostedService extension method:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
_workItems.Enqueue(workItem);
_signal.Release();
}
return workItem;
}
}
In QueueHostedService , background tasks in the queue are dequeued and executed as a BackgroundService, which
is a base class for implementing a long running IHostedService :
public class QueuedHostedService : BackgroundService
{
private readonly ILogger _logger;
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
The services are registered in Startup.ConfigureServices . The IHostedService implementation is registered with
the AddHostedService extension method:
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
In the Index page model class, the IBackgroundTaskQueue is injected into the constructor and assigned to Queue :
When the Add Task button is selected on the Index page, the OnPostAddTask method is executed.
QueueBackgroundWorkItem is called to enqueue the work item:
public IActionResult OnPostAddTask()
{
Queue.QueueBackgroundWorkItem(async token =>
{
var guid = Guid.NewGuid().ToString();
_logger.LogInformation(
$"Queued Background Task {guid} is complete. 3/3");
});
return RedirectToPage();
}
Additional resources
Implement background tasks in microservices with IHostedService and the BackgroundService class
System.Threading.Timer
Enhance an app from an external assembly in
ASP.NET Core with IHostingStartup
8/31/2018 • 13 minutes to read • Edit Online
By Luke Latham
An IHostingStartup (hosting startup) implementation adds enhancements to an app at startup from an external
assembly. For example, an external library can use a hosting startup implementation to provide additional
configuration providers or services to an app. IHostingStartup is available in ASP.NET Core 2.0 or later.
View or download sample code (how to download)
HostingStartup attribute
A HostingStartup attribute indicates the presence of a hosting startup assembly to activate at runtime.
The entry assembly or the assembly containing the Startup class is automatically scanned for the
HostingStartup attribute. The list of assemblies to search for HostingStartup attributes is loaded at runtime from
configuration in the WebHostDefaults.HostingStartupAssembliesKey. The list of assemblies to exclude from
discovery is loaded from the WebHostDefaults.HostingStartupExcludeAssembliesKey. For more information, see
Web Host: Hosting Startup Assemblies and Web Host: Hosting Startup Exclude Assemblies.
In the following example, the namespace of the hosting startup assembly is StartupEnhancement . The class
containing the hosting startup code is StartupEnhancementHostingStartup :
[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]
The HostingStartup attribute is typically located in the hosting startup assembly's IHostingStartup
implementation class file.
To disable automatic loading of hosting startup assemblies, set one of the following to true or 1 :
Prevent Hosting Startup host configuration setting.
ASPNETCORE_PREVENTHOSTINGSTARTUP environment variable.
If both the host configuration setting and the environment variable are set, the host setting controls the behavior.
Disabling hosting startup assemblies using the host setting or environment variable disables the assembly
globally and may disable several characteristics of an app.
Project
Create a hosting startup with either of the following project types:
Class library
Console app without an entry point
Class library
A hosting startup enhancement can be provided in a class library. The library contains a HostingStartup
attribute.
The sample code includes a Razor Pages app, HostingStartupApp, and a class library, HostingStartupLibrary. The
class library:
Contains a hosting startup class, ServiceKeyInjection , which implements IHostingStartup .
ServiceKeyInjection adds a pair of service strings to the app's configuration using the in-memory
configuration provider (AddInMemoryCollection).
Includes a HostingStartup attribute that identifies the hosting startup's namespace and class.
The ServiceKeyInjectionclass's Configure method uses an IWebHostBuilder to add enhancements to an app.
IHostingStartup.Configure in the hosting startup assembly is called by the runtime before Startup.Configure in
user code, which allows user code to overwrite any configuration provided by the hosting startup assembly.
HostingStartupLibrary/ServiceKeyInjection.cs:
[assembly: HostingStartup(typeof(HostingStartupLibrary.ServiceKeyInjection))]
namespace HostingStartupLibrary
{
public class ServiceKeyInjection : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config =>
{
var dict = new Dictionary<string, string>
{
{"DevAccount_FromLibrary", "DEV_1111111-1111"},
{"ProdAccount_FromLibrary", "PROD_2222222-2222"}
};
config.AddInMemoryCollection(dict);
});
}
}
}
The app's Index page reads and renders the configuration values for the two keys set by the class library's hosting
startup assembly:
HostingStartupApp/Pages/Index.cshtml.cs:
public class IndexModel : PageModel
{
public IndexModel(IConfiguration config)
{
ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
ServiceKey_Development_Package = config["DevAccount_FromPackage"];
ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
}
The sample code also includes a NuGet package project that provides a separate hosting startup,
HostingStartupPackage. The package has the same characteristics of the class library described earlier. The
package:
Contains a hosting startup class, ServiceKeyInjection , which implements IHostingStartup .
ServiceKeyInjection adds a pair of service strings to the app's configuration.
Includes a HostingStartup attribute.
HostingStartupPackage/ServiceKeyInjection.cs:
[assembly: HostingStartup(typeof(HostingStartupPackage.ServiceKeyInjection))]
namespace HostingStartupPackage
{
public class ServiceKeyInjection : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration(config =>
{
var dict = new Dictionary<string, string>
{
{"DevAccount_FromPackage", "DEV_3333333-3333"},
{"ProdAccount_FromPackage", "PROD_4444444-4444"}
};
config.AddInMemoryCollection(dict);
});
}
}
}
The app's Index page reads and renders the configuration values for the two keys set by the package's hosting
startup assembly:
HostingStartupApp/Pages/Index.cshtml.cs:
public class IndexModel : PageModel
{
public IndexModel(IConfiguration config)
{
ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
ServiceKey_Development_Package = config["DevAccount_FromPackage"];
ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions"
Version="2.1.1" />
</ItemGroup>
</Project>
A HostingStartup attribute identifies a class as an implementation of IHostingStartup for loading and execution
when building the IWebHost. In the following example, the namespace is StartupEnhancement , and the class is
StartupEnhancementHostingStartup :
[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]
A class implements IHostingStartup . The class's Configure method uses an IWebHostBuilder to add
enhancements to an app. IHostingStartup.Configure in the hosting startup assembly is called by the runtime
before Startup.Configure in user code, which allows user code to overwrite any configuration provided by the
hosting startup assembly.
namespace StartupEnhancement
{
public class StartupEnhancementHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
// Use the IWebHostBuilder to add app enhancements.
}
}
}
When building an IHostingStartup project, the dependencies file (*.deps.json) sets the runtime location of the
assembly to the bin folder:
"targets": {
".NETCoreApp,Version=v2.1": {
"StartupEnhancement/1.0.0": {
"dependencies": {
"Microsoft.AspNetCore.Hosting.Abstractions": "2.1.1"
},
"runtime": {
"StartupEnhancement.dll": {}
}
}
}
}
Only part of the file is shown. The assembly name in the example is StartupEnhancement .
HostingStartupLibrary;HostingStartupPackage;StartupDiagnostics
A hosting startup assembly can also be set using the Hosting Startup Assemblies host configuration setting.
When multiple hosting startup assembles are present, their Configure methods are executed in the order that the
assemblies are listed.
Activation
Options for hosting startup activation are:
Runtime store – Activation doesn't require a compile-time reference for activation. The sample app places the
hosting startup assembly and dependencies files into a folder, deployment, to facilitate deployment of the
hosting startup in a multimachine environment. The deployment folder also includes a PowerShell script that
creates or modifies environment variables on the deployment system to enable the hosting startup.
Compile-time reference required for activation
NuGet package
Project bin folder
Runtime store
The hosting startup implementation is placed in the runtime store. A compile-time reference to the assembly isn't
required by the enhanced app.
After the hosting startup is built, the hosting startup's project file serves as the manifest file for the dotnet store
command.
This command places the hosting startup assembly and other dependencies that aren't part of the shared
framework in the user profile's runtime store at:
Windows
macOS
Linux
%USERPROFILE%\.dotnet\store\x64\<TARGET_FRAMEWORK_MONIKER>\<ENHANCEMENT_ASSEMBLY_NAME>\
<ENHANCEMENT_VERSION>\lib\<TARGET_FRAMEWORK_MONIKER>\
If you desire to place the assembly and dependencies for global use, add the -o|--output option to the
dotnet store command with the following path:
Windows
macOS
Linux
%PROGRAMFILES%\dotnet\store\x64\<TARGET_FRAMEWORK_MONIKER>\<ENHANCEMENT_ASSEMBLY_NAME>\
<ENHANCEMENT_VERSION>\lib\<TARGET_FRAMEWORK_MONIKER>\
In the sample code (StartupDiagnostics project), modification of the *.deps.json file is performed by a PowerShell
script. The PowerShell script is automatically triggered by a build target in the project file.
The implementation's *.deps.json file must be in an accessible location.
For per-user use, place the file in the additonalDeps folder of the user profile's .dotnet settings:
Windows
macOS
Linux
%USERPROFILE%\.dotnet\x64\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\
<SHARED_FRAMEWORK_VERSION>\
For global use, place the file in the additonalDeps folder of the .NET Core installation:
Windows
macOS
Linux
%PROGRAMFILES%\dotnet\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\
<SHARED_FRAMEWORK_VERSION>\
The shared framework version reflects the version of the shared runtime that the target app uses. The shared
runtime is shown in the *.runtimeconfig.json file. In the sample app (HostingStartupApp), the shared runtime is
specified in the HostingStartupApp.runtimeconfig.json file.
List the hosting startup's dependencies file
The location of the implementation's *.deps.json file is listed in the DOTNET_ADDITIONAL_DEPS environment variable.
If the file is placed in the user profile's .dotnet folder, set the environment variable's value to:
Windows
macOS
Linux
%USERPROFILE%\.dotnet\x64\additionalDeps\
If the file is placed in the .NET Core installation for global use, provide the full path to the file:
Windows
macOS
Linux
%PROGRAMFILES%\dotnet\additionalDeps\<ENHANCEMENT_ASSEMBLY_NAME>\shared\Microsoft.NETCore.App\
<SHARED_FRAMEWORK_VERSION>\<ENHANCEMENT_ASSEMBLY_NAME>.deps.json
For the sample app (HostingStartupApp) to find the dependencies file (HostingStartupApp.runtimeconfig.json),
the dependencies file is placed in the user's profile.
Windows
macOS
Linux
Set the DOTNET_ADDITIONAL_DEPS environment variable to the following value:
%UserProfile%\.dotnet\x64\additionalDeps\StartupDiagnostics\
For examples of how to set environment variables for various operating systems, see Use multiple environments.
Deployment
To facilitate the deployment of a hosting startup in a multimachine environment, the sample app creates a
deployment folder in published output that contains:
The hosting startup assembly.
The hosting startup dependencies file.
A PowerShell script that creates or modifies the ASPNETCORE_HOSTINGSTARTUPASSEMBLIES and
DOTNET_ADDITIONAL_DEPS to support the activation of the hosting startup. Run the script from an administrative
PowerShell command prompt on the deployment system.
NuGet package
A hosting startup enhancement can be provided in a NuGet package. The package has a HostingStartup
attribute. The hosting startup types provided by the package are made available to the app using either of the
following approaches:
The enhanced app's project file makes a package reference for the hosting startup in the app's project file (a
compile-time reference). With the compile-time reference in place, the hosting startup assembly and all of its
dependencies are incorporated into the app's dependency file (*.deps.json). This approach applies to a hosting
startup assembly package published to nuget.org.
The hosting startup's dependencies file is made available to the enhanced app as described in the Runtime
store section (without a compile-time reference).
For more information on NuGet packages and the runtime store, see the following topics:
How to Create a NuGet Package with Cross Platform Tools
Publishing packages
Runtime package store
Project bin folder
A hosting startup enhancement can be provided by a bin-deployed assembly in the enhanced app. The hosting
startup types provided by the assembly are made available to the app using either of the following approaches:
The enhanced app's project file makes an assembly reference to the hosting startup (a compile-time
reference). With the compile-time reference in place, the hosting startup assembly and all of its dependencies
are incorporated into the app's dependency file (*.deps.json). This approach applies when the deployment
scenario calls for moving the compiled hosting startup library's assembly (DLL file) to the consuming project
or to a location accessible by the consuming project and a compile-time reference is made to the hosting
startup's assembly.
The hosting startup's dependencies file is made available to the enhanced app as described in the Runtime
store section (without a compile-time reference).
Sample code
The sample code (how to download) demonstrates hosting startup implementation scenarios:
Two hosting startup assemblies (class libraries) set a pair of in-memory configuration key-value pairs each:
NuGet package (HostingStartupPackage)
Class library ( HostingStartupLibrary)
A hosting startup is activated from a runtime store-deployed assembly ( StartupDiagnostics). The assembly
adds two middlewares to the app at startup that provide diagnostic information on:
Registered services
Address (scheme, host, path base, path, query string)
Connection (remote IP, remote port, local IP, local port, client certificate)
Request headers
Environment variables
To run the sample:
Activation from a NuGet package
1. Compile the HostingStartupPackage package with the dotnet pack command.
2. Add the package's assembly name of the HostingStartupPackage to the
ASPNETCORE_HOSTINGSTARTUPASSEMBLIES environment variable.
3. Compile and run the app. A package reference is present in the enhanced app (a compile-time reference).
A <PropertyGroup> in the app's project file specifies the package project's output
(../HostingStartupPackage/bin/Debug) as a package source. This allows the app to use the package
without uploading the package to nuget.org. For more information, see the notes in the
HostingStartupApp's project file.
<PropertyGroup>
<RestoreSources>$(RestoreSources);https://api.nuget.org/v3/index.json;../HostingStartupPackage/bin/Deb
ug</RestoreSources>
</PropertyGroup>
4. Observe that the service configuration key values rendered by the Index page match the values set by the
package's ServiceKeyInjection.Configure method.
If you make changes to the HostingStartupPackage project and recompile it, clear the local NuGet package
caches to ensure that the HostingStartupApp receives the updated package and not a stale package from the local
cache. To clear the local NuGet caches, execute the following dotnet nuget locals command:
<ItemGroup>
<Reference Include=".\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll">
<HintPath>.\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
</Reference>
</ItemGroup>
5. Observe that the service configuration key values rendered by the Index page match the values set by the
class library's ServiceKeyInjection.Configure method.
For Windows, the command uses the win7-x64 runtime identifier (RID ). When providing the hosting
startup for a different runtime, substitute the correct RID.
4. Set the environment variables:
Add the assembly name of StartupDiagnostics to the ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
environment variable.
On Windows, set the DOTNET_ADDITIONAL_DEPS environment variable to
%UserProfile%\.dotnet\x64\additionalDeps\StartupDiagnostics\ . On macOS/Linux, set the
DOTNET_ADDITIONAL_DEPS environment variable to
/Users/<USER>/.dotnet/x64/additionalDeps/StartupDiagnostics/ , where <USER> is the user profile that
contains the hosting startup.
5. Run the sample app.
6. Request the /services endpoint to see the app's registered services. Request the /diag endpoint to see
the diagnostic information.
Web server implementations in ASP.NET Core
9/18/2018 • 5 minutes to read • Edit Online
Kestrel
Kestrel is the default web server included in ASP.NET Core project templates.
Kestrel can be used by itself or with a reverse proxy server, such as IIS, Nginx, or Apache. A reverse proxy server
receives HTTP requests from the Internet and forwards them to Kestrel after some preliminary handling.
Either configuration—with or without a reverse proxy server—is a valid and supported hosting configuration for
ASP.NET Core 2.0 or later apps. For more information, see When to use Kestrel with a reverse proxy.
If the app only accepts requests from an internal network, Kestrel can be used by itself.
If the app is exposed to the Internet, Kestrel must use IIS, Nginx, or Apache as a reverse proxy server. A reverse
proxy server receives HTTP requests from the Internet and forwards them to Kestrel after some preliminary
handling, as shown in the following diagram:
The most important reason for using a reverse proxy for edge deployments (exposed to traffic from the Internet)
is security. The 1.x versions of Kestrel don't have important security features to defend against attacks from the
Internet. This includes, but isn't limited to, appropriate timeouts, request size limits, and concurrent connection
limits.
For more information, see When to use Kestrel with a reverse proxy.
IIS, Nginx, and Apache can't be used without Kestrel or a custom server implementation. ASP.NET Core was
designed to run in its own process so that it can behave consistently across platforms. IIS, Nginx, and Apache
dictate their own startup procedure and environment. To use these server technologies directly, ASP.NET Core
would need to adapt to the requirements of each server. Using a web server implementation, such as Kestrel,
ASP.NET Core has control over the startup process and environment when hosted on different server
technologies.
IIS with Kestrel
When using IIS or IIS Express as a reverse proxy for ASP.NET Core, the ASP.NET Core app runs in a process
separate from the IIS worker process. In the IIS process, the ASP.NET Core Module coordinates the reverse
proxy relationship. The primary functions of the ASP.NET Core Module are to start the ASP.NET Core app,
restart the app when it crashes, and forward HTTP traffic to the app. For more information, see ASP.NET Core
Module.
Nginx with Kestrel
For information on how to use Nginx on Linux as a reverse proxy server for Kestrel, see Host on Linux with
Nginx.
Apache with Kestrel
For information on how to use Apache on Linux as a reverse proxy server for Kestrel, see Host on Linux with
Apache.
HTTP.sys
If ASP.NET Core apps are run on Windows, HTTP.sys is an alternative to Kestrel. Kestrel is generally
recommended for best performance. HTTP.sys can be used in scenarios where the app is exposed to the Internet
and required capabilities are supported by HTTP.sys but not Kestrel. For information on HTTP.sys, see HTTP.sys.
HTTP.sys can also be used for apps that are only exposed to an internal network.
HTTP.sys is named WebListener in ASP.NET Core 1.x. If ASP.NET Core apps are run on Windows, WebListener
is an alternative for scenarios where IIS isn't available to host apps.
WebListener can also be used in place of Kestrel for apps that are only exposed to an internal network, if required
capabilities are supported by WebListener but not Kestrel. For information on WebListener, see WebListener.
Custom servers
If the built-in servers don't meet the app's requirements, a custom server implementation can be created. The
Open Web Interface for .NET (OWIN ) guide demonstrates how to write a Nowin-based IServer implementation.
Only the feature interfaces that the app uses require implementation, though at a minimum IHttpRequestFeature
and IHttpResponseFeature must be supported.
Server startup
When using Visual Studio, Visual Studio for Mac, or Visual Studio Code, the server is launched when the app is
started by the Integrated Development Environment (IDE ). In Visual Studio on Windows, launch profiles can be
used to start the app and server with either IIS Express/ASP.NET Core Module or the console. In Visual Studio
Code, the app and server are started by Omnisharp, which activates the CoreCLR debugger. Using Visual Studio
for Mac, the app and server are started by the Mono Soft-Mode Debugger.
When launching an app from a command prompt in the project's folder,dotnet run launches the app and server
(Kestrel and HTTP.sys only). The configuration is specified by the -c|--configuration option, which is set to either
Debug (default) or Release . If launch profiles are present in a launchSettings.json file, use the
--launch-profile <NAME> option to set the launch profile (for example, Development or Production ). For more
information, see the dotnet run and .NET Core distribution packaging topics.
HTTP/2 support
HTTP/2 is supported with ASP.NET Core in the following deployment scenarios:
Kestrel
Operating system
Windows Server 2012 R2/Windows 8.1 or later
Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
HTTP/2 will be supported on macOS in a future release.
Target framework: .NET Core 2.2 or later
HTTP.sys
Windows Server 2016/Windows 10 or later
Target framework: Not applicable to HTTP.sys deployments.
IIS (in-process)
Windows Server 2016/Windows 10 or later; IIS 10 or later
Target framework: .NET Core 2.2 or later
IIS (out-of-process)
Windows Server 2016/Windows 10 or later; IIS 10 or later
Edge connections use HTTP/2, but the reverse proxy connection to Kestrel uses HTTP/1.1.
Target framework: Not applicable to IIS out-of-process deployments.
HTTP.sys
Windows Server 2016/Windows 10 or later
Target framework: Not applicable to HTTP.sys deployments.
IIS (out-of-process)
Windows Server 2016/Windows 10 or later; IIS 10 or later
Edge connections use HTTP/2, but the reverse proxy connection to Kestrel uses HTTP/1.1.
Target framework: Not applicable to IIS out-of-process deployments.
An HTTP/2 connection must use Application-Layer Protocol Negotiation (ALPN ) and TLS 1.2 or later. For more
information, see the topics that pertain to your server deployment scenarios.
Additional resources
Kestrel web server implementation in ASP.NET Core
ASP.NET Core Module
Host ASP.NET Core on Windows with IIS
Host ASP.NET Core on Azure App Service
Host ASP.NET Core on Linux with Nginx
Host ASP.NET Core on Linux with Apache
HTTP.sys web server implementation in ASP.NET Core (for ASP.NET Core 1.x, see WebListener web server
implementation in ASP.NET Core)
Kestrel web server implementation in ASP.NET Core
9/26/2018 • 26 minutes to read • Edit Online
HTTP/2 support
HTTP/2 is available for ASP.NET Core apps if the following base requirements are met:
Operating system†
Windows Server 2012 R2/Windows 8.1 or later
Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
Target framework: .NET Core 2.2 or later
Application-Layer Protocol Negotiation (ALPN ) connection
TLS 1.2 or later connection
†HTTP/2 will be supported on macOS in a future release.
If an HTTP/2 connection is established, HttpRequest.Protocol reports HTTP/2 .
HTTP/2 is disabled by default. For more information on configuration, see the Kestrel options and Endpoint
configuration sections.
If you expose your app to the Internet, use IIS, Nginx, or Apache as a reverse proxy server. A reverse proxy
server receives HTTP requests from the Internet and forwards them to Kestrel after some preliminary
handling.
A reverse proxy is required for edge deployments (exposed to traffic from the Internet) for security reasons.
The 1.x versions of Kestrel don't have a full complement of defenses against attacks, such as appropriate
timeouts, size limits, and concurrent connection limits.
A reverse proxy scenario exists when there are multiple apps that share the same IP and port running on a
single server. Kestrel doesn't support this scenario because Kestrel doesn't support sharing the same IP and
port among multiple processes. When Kestrel is configured to listen on a port, Kestrel handles all of the traffic
for that port regardless of requests' host header. A reverse proxy that can share ports has the ability to forward
requests to Kestrel on a unique IP and port.
Even if a reverse proxy server isn't required, using a reverse proxy server might be a good choice:
It can limit the exposed public surface area of the apps that it hosts.
It provides an additional layer of configuration and defense.
It might integrate better with existing infrastructure.
It simplifies load balancing and SSL configuration. Only the reverse proxy server requires an SSL
certificate, and that server can communicate with your app servers on the internal network using plain
HTTP.
WARNING
If not using a reverse proxy with host filtering enabled, host filtering must be enabled.
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
});
There's a separate limit for connections that have been upgraded from HTTP or HTTPS to another protocol
(for example, on a WebSockets request). After a connection is upgraded, it isn't counted against the
MaxConcurrentConnections limit.
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
Here's an example that shows how to configure the constraint for the app on every request:
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
});
An exception is thrown if you attempt to configure the limit on a request after the app has started to read the
request. There's an IsReadOnly property that indicates if the MaxRequestBodySize property is in read-only state,
meaning it's too late to configure the limit.
Minimum request body data rate
MinRequestBodyDataRate
MinResponseDataRate
Kestrel checks every second if data is arriving at the specified rate in bytes/second. If the rate drops below the
minimum, the connection is timed out. The grace period is the amount of time that Kestrel gives the client to
increase its send rate up to the minimum; the rate isn't checked during that time. The grace period helps avoid
dropping connections that are initially sending data at a slow rate due to TCP slow -start.
The default minimum rate is 240 bytes/second with a 5 second grace period.
A minimum rate also applies to the response. The code to set the request limit and the response limit is the
same except for having RequestBody or Response in the property and interface names.
Here's an example that shows how to configure the minimum data rates in Program.cs:
.UseKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 100;
options.Limits.MaxConcurrentUpgradedConnections = 100;
options.Limits.MaxRequestBodySize = 10 * 1024;
options.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testPassword");
});
});
Endpoint configuration
By default, ASP.NET Core binds to http://localhost:5000 . Call Listen or ListenUnixSocket methods on
KestrelServerOptions to configure URL prefixes and ports for Kestrel. UseUrls , the --urls command-line
argument, urls host configuration key, and the ASPNETCORE_URLS environment variable also work but have the
limitations noted later in this section.
The urls host configuration key must come from the host configuration, not the app configuration. Adding a
urls key and value to appsettings.json doesn't affect host configuration because the host is completely
initialized by the time the configuration is read from the configuration file. However, a urls key in
appsettings.json can be used with UseConfiguration on the host builder to configure the host:
ListenOptions.UseHttps parameters:
filename is the path and file name of a certificate file, relative to the directory that contains the app's
content files.
password is the password required to access the X.509 certificate data.
configureOptions is an Action to configure the HttpsConnectionAdapterOptions . Returns the ListenOptions
.
storeName is the certificate store from which to load the certificate.
subject is the subject name for the certificate.
allowInvalid indicates if invalid certificates should be considered, such as self-signed certificates.
location is the store location to load the certificate from.
serverCertificate is the X.509 certificate.
In production, HTTPS must be explicitly configured. At a minimum, a default certificate must be provided.
Supported configurations described next:
No configuration
Replace the default certificate from configuration
Change the defaults in code
No configuration
Kestrel listens on http://localhost:5000 and https://localhost:5001 (if a default cert is available).
Specify URLs using the:
ASPNETCORE_URLS environment variable.
--urls command-line argument.
urls host configuration key.
UseUrls extension method.
"HttpsInlineCertFile": {
"Url": "https://localhost:5001",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
},
"HttpsInlineCertStore": {
"Url": "https://localhost:5002",
"Certificate": {
"Subject": "<subject; required>",
"Store": "<certificate store; defaults to My>",
"Location": "<location; defaults to CurrentUser>",
"AllowInvalid": "<true or false; defaults to false>"
}
},
"HttpsDefaultCert": {
"Url": "https://localhost:5003"
},
"Https": {
"Url": "https://*:5004",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
},
"Certificates": {
"Default": {
"Path": "<path to .pfx file>",
"Password": "<certificate password>"
}
}
}
}
An alternative to using Path and Password for any certificate node is to specify the certificate using certificate
store fields. For example, the Certificates > Default certificate can be specified as:
"Default": {
"Subject": "<subject; required>",
"Store": "<cert store; defaults to My>",
"Location": "<location; defaults to CurrentUser>",
"AllowInvalid": "<true or false; defaults to false>"
}
Schema notes:
Endpoints names are case-insensitive. For example, HTTPS and Https are valid.
The Url parameter is required for each endpoint. The format for this parameter is the same as the top-
level Urls configuration parameter except that it's limited to a single value.
These endpoints replace those defined in the top-level Urls configuration rather than adding to them.
Endpoints defined in code via Listen are cumulative with the endpoints defined in the configuration
section.
The Certificate section is optional. If the Certificate section isn't specified, the defaults defined in
earlier scenarios are used. If no defaults are available, the server throws an exception and fails to start.
The Certificate section supports both Path–Password and Subject–Store certificates.
Any number of endpoints may be defined in this way so long as they don't cause port conflicts.
options.Configure(context.Configuration.GetSection("Kestrel")) returns a KestrelConfigurationLoader
with an .Endpoint(string name, options => { }) method that can be used to supplement a configured
endpoint's settings:
options.Configure(context.Configuration.GetSection("Kestrel"))
.Endpoint("HTTPS", opt =>
{
opt.HttpsOptions.SslProtocols = SslProtocols.Tls12;
});
You can also directly access KestrelServerOptions.ConfigurationLoader to keep iterating on the existing
loader, such as the one provided by WebHost.CreateDefaultBuilder.
The configuration section for each endpoint is a available on the options in the Endpoint method so
that custom settings may be read.
Multiple configurations may be loaded by calling
options.Configure(context.Configuration.GetSection("Kestrel")) again with another section. Only the
last configuration is used, unless Load is explicitly called on prior instances. The metapackage doesn't
call Load so that its default configuration section may be replaced.
KestrelConfigurationLoader mirrors the Listen family of APIs from KestrelServerOptions as
Endpoint overloads, so code and config endpoints may be configured in the same place. These
overloads don't use names and only consume default settings from configuration.
Change the defaults in code
ConfigureEndpointDefaults and can be used to change default settings for
ConfigureHttpsDefaults
ListenOptions and HttpsConnectionAdapterOptions , including overriding the default certificate specified in the
prior scenario. ConfigureEndpointDefaults and ConfigureHttpsDefaults should be called before any endpoints
are configured.
options.ConfigureEndpointDefaults(opt =>
{
opt.NoDelay = true;
});
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.SslProtocols = SslProtocols.Tls12;
});
return exampleCert;
};
});
});
});
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel((context, options) =>
{
options.ListenAnyIP(5005, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
var localhostCert = CertificateLoader.LoadFromStoreCert(
"localhost", "My", StoreLocation.CurrentUser,
allowInvalid: true);
var exampleCert = CertificateLoader.LoadFromStoreCert(
"example.com", "My", StoreLocation.CurrentUser,
allowInvalid: true);
var subExampleCert = CertificateLoader.LoadFromStoreCert(
"sub.example.com", "My", StoreLocation.CurrentUser,
allowInvalid: true);
var certs = new Dictionary<string, X509Certificate2>(
StringComparer.OrdinalIgnoreCase);
certs["localhost"] = localhostCert;
certs["example.com"] = exampleCert;
certs["sub.example.com"] = subExampleCert;
return exampleCert;
};
});
});
})
.Build();
The example configures SSL for an endpoint with ListenOptions. Use the same API to configure other Kestrel
settings for specific endpoints.
On Windows, self-signed certificates can be created using the New -SelfSignedCertificate PowerShell cmdlet.
For an unsupported example, see UpdateIISExpressSSLForChrome.ps1.
On macOS, Linux, and Windows, certificates can be created using OpenSSL.
Bind to a Unix socket
Listen on a Unix socket with ListenUnixSocket for improved performance with Nginx, as shown in this
example:
.UseKestrel(options =>
{
options.ListenUnixSocket("/tmp/kestrel-test.sock");
options.ListenUnixSocket("/tmp/kestrel-test.sock", listenOptions =>
{
listenOptions.UseHttps("testCert.pfx", "testpassword");
});
});
Port 0
When the port number 0 is specified, Kestrel dynamically binds to an available port. The following example
shows how to determine which port Kestrel actually bound at runtime:
public void Configure(IApplicationBuilder app)
{
var serverAddressesFeature =
app.ServerFeatures.Get<IServerAddressesFeature>();
app.UseStaticFiles();
if (serverAddressesFeature != null)
{
await context.Response
.WriteAsync("<p>Listening on the following addresses: " +
string.Join(", ", serverAddressesFeature.Addresses) +
"</p>");
}
When the app is run, the console window output indicates the dynamic port where the app can be reached:
Limitations
Configure endpoints with the following approaches:
UseUrls
--urls command-line argument
urls host configuration key
ASPNETCORE_URLS environment variable
These methods are useful for making code work with servers other than Kestrel. However, be aware of the
following limitations:
SSL can't be used with these approaches unless a default certificate is provided in the HTTPS endpoint
configuration (for example, using KestrelServerOptions configuration or a configuration file as shown
earlier in this topic).
When both the Listen and UseUrls approaches are used simultaneously, the Listen endpoints override
the UseUrls endpoints.
IIS endpoint configuration
When using IIS, the URL bindings for IIS override bindings are set by either Listen or UseUrls . For more
information, see the ASP.NET Core Module topic.
By default, ASP.NET Core binds to http://localhost:5000 . Configure URL prefixes and ports for Kestrel using:
UseUrls extension method
--urls command-line argument
urls host configuration key
ASP.NET Core configuration system, including ASPNETCORE_URLS environment variable
For more information on these methods, see Hosting.
IIS endpoint configuration
When using IIS, the URL bindings for IIS override bindings set by UseUrls . For more information, see the
ASP.NET Core Module topic.
ListenOptions.Protocols
The Protocols property establishes the HTTP protocols ( HttpProtocols ) enabled on a connection endpoint or
for the server. Assign a value to the Protocols property from the HttpProtocols enum.
Http2 HTTP/2 only. Primarily used with TLS. May be used without
TLS only if the client supports a Prior Knowledge mode.
The following configuration file example establishes a connection protocol for a specific endpoint:
{
"Kestrel": {
"EndPoints": {
"HttpsDefaultCert": {
"Url": "https://localhost:5001",
"Protocols": "Http1AndHttp2"
}
}
}
}
Transport configuration
With the release of ASP.NET Core 2.1, Kestrel's default transport is no longer based on Libuv but instead
based on managed sockets. This is a breaking change for ASP.NET Core 2.0 apps upgrading to 2.1 that call
WebHostBuilderLibuvExtensions.UseLibuv and depend on either of the following packages:
Microsoft.AspNetCore.Server.Kestrel (direct package reference)
Microsoft.AspNetCore.App
For ASP.NET Core 2.1 or later projects that use the Microsoft.AspNetCore.App metapackage and require the
use of Libuv:
Add a dependency for the Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv package to the app's
project file:
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv"
Version="<LATEST_VERSION>" />
Call WebHostBuilderLibuvExtensions.UseLibuv:
URL prefixes
When using UseUrls , --urls command-line argument, urls host configuration key, or ASPNETCORE_URLS
environment variable, the URL prefixes can be in any of the following formats.
Only HTTP URL prefixes are valid. Kestrel doesn't support SSL when configuring URL bindings using
UseUrls .
http://65.55.39.10:80/
http://[0:0:0:0:0:ffff:4137:270a]:80/
http://contoso.com:80/
http://*:80/
Host names, * , and + , aren't special. Anything not recognized as a valid IP address or localhost
binds to all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the
same port, use HTTP.sys or a reverse proxy server, such as IIS, Nginx, or Apache.
WARNING
If not using a reverse proxy with host filtering enabled, enable host filtering.
Host localhost name with port number or loopback IP with port number
http://localhost:5000/
http://127.0.0.1:5000/
http://[::1]:5000/
When localhost is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the
requested port is in use by another service on either loopback interface, Kestrel fails to start. If either
loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported),
Kestrel logs a warning.
IPv4 address with port number
http://65.55.39.10:80/
https://65.55.39.10:443/
http://contoso.com:80/
http://*:80/
https://contoso.com:443/
https://*:443/
Host names, * , and + aren't special. Anything that isn't a recognized IP address or localhost binds to
all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the same port,
use WebListener or a reverse proxy server, such as IIS, Nginx, or Apache.
Host localhost name with port number or loopback IP with port number
http://localhost:5000/
http://127.0.0.1:5000/
http://[::1]:5000/
When localhost is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the
requested port is in use by another service on either loopback interface, Kestrel fails to start. If either
loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported),
Kestrel logs a warning.
Unix socket
http://unix:/run/dan-live.sock
Port 0
When the port number is 0 is specified, Kestrel dynamically binds to an available port. Binding to port 0 is
allowed for any host name or IP except for localhost .
When the app is run, the console window output indicates the dynamic port where the app can be reached:
On Windows, self-signed certificates can be created using the New -SelfSignedCertificate PowerShell cmdlet.
For an unsupported example, see UpdateIISExpressSSLForChrome.ps1.
On macOS, Linux, and Windows, certificates can be created using OpenSSL.
Host filtering
While Kestrel supports configuration based on prefixes such as http://example.com:5000 , Kestrel largely
ignores the host name. Host localhost is a special case used for binding to loopback addresses. Any host
other than an explicit IP address binds to all public IP addresses. None of this information is used to validate
request Host headers.
As a workaround, host behind a reverse proxy with host header filtering. This is the only supported scenario
for Kestrel in ASP.NET Core 1.x.
As a workaround, use middleware to filter requests by the Host header:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
// A normal middleware would provide an options type, config binding, extension methods, etc..
// This intentionally does all of the work inside of the middleware so it can be
// easily copy-pasted into docs and other projects.
public class HostFilteringMiddleware
{
private readonly RequestDelegate _next;
private readonly IList<string> _hosts;
private readonly ILogger<HostFilteringMiddleware> _logger;
return _next(context);
}
// This does not duplicate format validations that are expected to be performed by the host.
private bool ValidateHost(HttpContext context)
{
StringSegment host = context.Request.Headers[HeaderNames.Host].ToString().Trim();
if (StringSegment.IsNullOrEmpty(host))
{
// Http/1.0 does not require the Host header.
// Http/1.1 requires the header but the value may be empty.
return true;
}
if (colonIndex > 0)
{
host = host.Subsegment(0, colonIndex);
}
return false;
}
}
}
Register the preceding HostFilteringMiddleware in Startup.Configure . Note that the ordering of middleware
registration is important. Registration should occur immediately after Diagnostic Middleware registration (for
example, app.UseExceptionHandler ).
app.UseMiddleware<HostFilteringMiddleware>();
app.UseMvcWithDefaultRoute();
}
Host Filtering Middleware is disabled by default. To enable the middleware, define an AllowedHosts key in
appsettings.json/appsettings.<EnvironmentName>.json. The value is a semicolon-delimited list of host names
without port numbers:
appsettings.json:
{
"AllowedHosts": "example.com;localhost"
}
NOTE
Forwarded Headers Middleware also has an ForwardedHeadersOptions.AllowedHosts option. Forwarded Headers
Middleware and Host Filtering Middleware have similar functionality for different scenarios. Setting AllowedHosts with
Forwarded Headers Middleware is appropriate when the Host header isn't preserved while forwarding requests with a
reverse proxy server or load balancer. Setting AllowedHosts with Host Filtering Middleware is appropriate when Kestrel
is used as an edge server or when the Host header is directly forwarded.
For more information on Forwarded Headers Middleware, see Configure ASP.NET Core to work with proxy servers and
load balancers.
Additional resources
Enforce HTTPS
Kestrel source code
RFC 7230: Message Syntax and Routing (Section 5.4: Host)
Configure ASP.NET Core to work with proxy servers and load balancers
ASP.NET Core Module
6/21/2018 • 2 minutes to read • Edit Online
Requests arrive from the web to the kernel-mode HTTP.sys driver. The driver routes the requests to IIS on the
website's configured port, usually 80 (HTTP ) or 443 (HTTPS ). The module forwards the requests to Kestrel on
a random port for the app, which isn't port 80/443.
The module specifies the port via an environment variable at startup, and the IIS Integration Middleware
configures the server to listen on http://localhost:{port} . Additional checks are performed, and requests
that don't originate from the module are rejected. The module doesn't support HTTPS forwarding, so requests
are forwarded over HTTP even if received by IIS over HTTPS.
After Kestrel picks up a request from the module, the request is pushed into the ASP.NET Core middleware
pipeline. The middleware pipeline handles the request and passes it on as an HttpContext instance to the
app's logic. The app's response is passed back to IIS, which pushes it back out to the HTTP client that initiated
the request.
The ASP.NET Core Module has a few other functions. The module can:
Set environment variables for the worker process.
Log stdout output to file storage for troubleshooting startup issues.
Forward Windows authentication tokens.
Additional resources
Host on Windows with IIS
ASP.NET Core Module configuration reference
ASP.NET Core Module GitHub repository (source code)
HTTP.sys web server implementation in ASP.NET
Core
9/18/2018 • 7 minutes to read • Edit Online
NOTE
This topic applies to ASP.NET Core 2.0 or later. In earlier versions of ASP.NET Core, HTTP.sys is named WebListener.
HTTP.sys is a web server for ASP.NET Core that only runs on Windows. HTTP.sys is an alternative to Kestrel
and offers some features that Kestrel doesn't provide.
IMPORTANT
HTTP.sys is incompatible with the ASP.NET Core Module and can't be used with IIS or IIS Express.
An internal deployment requires a feature not available in Kestrel, such as Windows Authentication.
HTTP.sys is mature technology that protects against many types of attacks and provides the robustness,
security, and scalability of a full-featured web server. IIS itself runs as an HTTP listener on top of HTTP.sys.
HTTP/2 support
HTTP/2 is enabled for ASP.NET Core apps if the following base requirements are met:
Windows Server 2016/Windows 10 or later
Application-Layer Protocol Negotiation (ALPN ) connection
TLS 1.2 or later connection
If an HTTP/2 connection is established, HttpRequest.Protocol reports HTTP/2 .
If an HTTP/2 connection is established, HttpRequest.Protocol reports HTTP/1.1 .
HTTP/2 is enabled by default. If an HTTP/2 connection isn't established, the connection falls back to HTTP/1.1.
In a future release of Windows, HTTP/2 configuration flags will be available, including the ability to disable
HTTP/2 with HTTP.sys.
MaxRequestBodySize
The maximum allowed size of any request body in bytes. When set to null , the maximum request body
size is unlimited. This limit has no effect on upgraded connections, which are always unlimited.
The recommended method to override the limit in an ASP.NET Core MVC app for a single
IActionResult is to use the RequestSizeLimitAttribute attribute on an action method:
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
An exception is thrown if the app attempts to configure the limit on a request after the app has started
reading the request. An IsReadOnly property can be used to indicate if the MaxRequestBodySize property
is in a read-only state, meaning it's too late to configure the limit.
If the app should override MaxRequestBodySize per-request, use the IHttpMaxRequestBodySizeFeature:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILogger<Startup> logger)
{
app.Use(async (context, next) =>
{
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
.MaxRequestBodySize = 10 * 1024;
logger.LogInformation($"Addresses: {addresses}");
await next.Invoke();
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
3. If using Visual Studio, make sure the app isn't configured to run IIS or IIS Express.
In Visual Studio, the default launch profile is for IIS Express. To run the project as a console app, manually
change the selected profile, as shown in the following screen shot:
WARNING
Top-level wildcard bindings ( http://*:80/ and http://+:80 ) should not be used. Top-level wildcard bindings
can open up your app to security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host
names rather than wildcards. Subdomain wildcard binding (for example, *.mysub.com ) doesn't have this security
risk if you control the entire parent domain (as opposed to *.com , which is vulnerable). See rfc7230 section-5.4
for more information.
Additional resources
HTTP Server API
aspnet/HttpSysServer GitHub repository (source code)
Host in ASP.NET Core
Session and app state in ASP.NET Core
8/22/2018 • 17 minutes to read • Edit Online
State management
State can be stored using several approaches. Each approach is described later in this topic.
Cookies HTTP cookies (may include data stored using server-side app
code)
Cookies
Cookies store data across requests. Because cookies are sent with every request, their size should be kept to a
minimum. Ideally, only an identifier should be stored in a cookie with the data stored by the app. Most browsers
restrict cookie size to 4096 bytes. Only a limited number of cookies are available for each domain.
Because cookies are subject to tampering, they must be validated by the app. Cookies can be deleted by users and
expire on clients. However, cookies are generally the most durable form of data persistence on the client.
Cookies are often used for personalization, where content is customized for a known user. The user is only
identified and not authenticated in most cases. The cookie can store the user's name, account name, or unique
user ID (such as a GUID ). You can then use the cookie to access the user's personalized settings, such as their
preferred website background color.
Be mindful of the European Union General Data Protection Regulations (GDPR ) when issuing cookies and
dealing with privacy concerns. For more information, see General Data Protection Regulation (GDPR ) support in
ASP.NET Core.
Session state
Session state is an ASP.NET Core scenario for storage of user data while the user browses a web app. Session
state uses a store maintained by the app to persist data across requests from a client. The session data is backed
by a cache and considered ephemeral data—the site should continue to function without the session data.
NOTE
Session isn't supported in SignalR apps because a SignalR Hub may execute independent of an HTTP context. For example,
this can occur when a long polling request is held open by a hub beyond the lifetime of the request's HTTP context.
ASP.NET Core maintains session state by providing a cookie to the client that contains a session ID, which is sent
to the app with each request. The app uses the session ID to fetch the session data.
Session state exhibits the following behaviors:
Because the session cookie is specific to the browser, sessions aren't shared across browsers.
Session cookies are deleted when the browser session ends.
If a cookie is received for an expired session, a new session is created that uses the same session cookie.
Empty sessions aren't retained—the session must have at least one value set into it to persist the session
across requests. When a session isn't retained, a new session ID is generated for each new request.
The app retains a session for a limited time after the last request. The app either sets the session timeout or
uses the default value of 20 minutes. Session state is ideal for storing user data that's specific to a particular
session but where the data doesn't require permanent storage across sessions.
Session data is deleted either when the ISession.Clear implementation is called or when the session expires.
There's no default mechanism to inform app code that a client browser has been closed or when the session
cookie is deleted or expired on the client.
WARNING
Don't store sensitive data in session state. The user might not close the browser and clear the session cookie. Some
browsers maintain valid session cookies across browser windows. A session might not be restricted to a single user—the
next user might continue to browse the app with the same session cookie.
The in-memory cache provider stores session data in the memory of the server where the app resides. In a server
farm scenario:
Use sticky sessions to tie each session to a specific app instance on an individual server. Azure App Service uses
Application Request Routing (ARR ) to enforce sticky sessions by default. However, sticky sessions can affect
scalability and complicate web app updates. A better approach is to use a Redis or SQL Server distributed
cache, which doesn't require sticky sessions. For more information, see Work with a distributed cache.
The session cookie is encrypted via IDataProtector. Data Protection must be properly configured to read
session cookies on each machine. For more information, see Data Protection in ASP.NET Core and Key
storage providers.
Configure session state
The Microsoft.AspNetCore.Session package, which is included in the Microsoft.AspNetCore.App metapackage,
provides middleware for managing session state. To enable the session middleware, Startup must contain:
The Microsoft.AspNetCore.Session package provides middleware for managing session state. To enable the
session middleware, Startup must contain:
Any of the IDistributedCache memory caches. The IDistributedCache implementation is used as a backing
store for session. For more information, see Work with a distributed cache.
A call to AddSession in ConfigureServices .
A call to UseSession in Configure .
The following code shows how to set up the in-memory session provider with a default in-memory
implementation of IDistributedCache :
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.UseHttpContextItemsMiddleware();
app.UseMvc();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.CookieHttpOnly = true;
});
services.AddMvc();
}
The order of middleware is important. In the preceding example, an InvalidOperationException exception occurs
when UseSession is invoked after UseMvc . For more information, see Middleware Ordering.
HttpContext.Session is available after session state is configured.
HttpContext.Session can't be accessed before UseSession has been called.
A new session with a new session cookie can't be created after the app has begun writing to the response stream.
The exception is recorded in the web server log and not displayed in the browser.
Load session state asynchronously
The default session provider in ASP.NET Core loads session records from the underlying IDistributedCache
backing store asynchronously only if the ISession.LoadAsync method is explicitly called before the TryGetValue,
Set, or Remove methods. If LoadAsync isn't called first, the underlying session record is loaded synchronously,
which can incur a performance penalty at scale.
To have apps enforce this pattern, wrap the DistributedSessionStore and DistributedSession implementations
with versions that throw an exception if the LoadAsync method isn't called before TryGetValue , Set , or Remove .
Register the wrapped versions in the services container.
Session options
To override session defaults, use SessionOptions.
OPTION DESCRIPTION
IdleTimeout The IdleTimeout indicates how long the session can be idle
before its contents are abandoned. Each session access resets
the timeout. Note this only applies to the content of the
session, not the cookie. The default is 20 minutes.
Session uses a cookie to track and identify requests from a single browser. By default, this cookie is named
.AspNetCore.Session , and it uses a path of / . Because the cookie default doesn't specify a domain, it isn't made
available to the client-side script on the page (because HttpOnly defaults to true ).
OPTION DESCRIPTION
CookieName Determines the cookie name used to persist the session ID.
The default is SessionDefaults.CookieName (
.AspNetCore.Session ).
IdleTimeout The IdleTimeout indicates how long the session can be idle
before its contents are abandoned. Each session access resets
the timeout. Note this only applies to the content of the
session, not the cookie. The default is 20 minutes.
Session uses a cookie to track and identify requests from a single browser. By default, this cookie is named
.AspNet.Session , and it uses a path of / .
services.AddDistributedMemoryCache();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSession(options =>
{
options.Cookie.Name = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
});
}
services.AddSession(options =>
{
options.CookieName = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
});
services.AddMvc();
}
The app uses the IdleTimeout property to determine how long a session can be idle before its contents in the
server's cache are abandoned. This property is independent of the cookie expiration. Each request that passes
through the Session Middleware resets the timeout.
Session state is non-locking. If two requests simultaneously attempt to modify the contents of a session, the last
request overrides the first. Session is implemented as a coherent session, which means that all the contents are
stored together. When two requests seek to modify different session values, the last request may override session
changes made by the first.
Set and get Session values
Session state is accessed from a Razor Pages PageModel class or MVC Controller class with HttpContext.Session.
This property is an ISession implementation.
The ISession implementation provides several extension methods to set and retreive integer and string values.
The extension methods are in the Microsoft.AspNetCore.Http namespace (add a
using Microsoft.AspNetCore.Http; statement to gain access to the extension methods) when the
Microsoft.AspNetCore.Http.Extensions package is referenced by the project. Both packages are included in the
Microsoft.AspNetCore.App metapackage.
The ISession implementation provides several extension methods to set and retreive integer and string values.
The extension methods are in the Microsoft.AspNetCore.Http namespace (add a
using Microsoft.AspNetCore.Http; statement to gain access to the extension methods) when the
Microsoft.AspNetCore.Http.Extensions package is referenced by the project.
ISession extension methods:
Get(ISession, String)
GetInt32(ISession, String)
GetString(ISession, String)
SetInt32(ISession, String, Int32)
SetString(ISession, String, String)
The following example retrieves the session value for the IndexModel.SessionKeyName key ( _Name in the sample
app) in a Razor Pages page:
@page
@using Microsoft.AspNetCore.Http
@model IndexModel
...
Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)
The following example shows how to set and get an integer and a string:
return RedirectToAction("SessionNameYears");
}
All session data must be serialized to enable a distributed cache scenario, even when using the in-memory cache.
Minimal string and number serializers are provided (see the methods and extension methods of ISession).
Complex types must be serialized by the user using another mechanism, such as JSON.
Add the following extension methods to set and get serializable objects:
The following example shows how to set and get a serializable object with the extension methods:
// Requires you add the Set and Get extension method mentioned in the topic.
if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default(DateTime))
{
HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
}
return RedirectToAction("GetDate");
}
TempData
ASP.NET Core exposes the TempData property of a Razor Pages page model or TempData of an MVC controller.
This property stores data until it's read. The Keep and Peek methods can be used to examine the data without
deletion. TempData is particularly useful for redirection when data is required for more than a single request.
TempData is implemented by TempData providers using either cookies or session state.
TempData providers
In ASP.NET Core 2.0 or later, the cookie-based TempData provider is used by default to store TempData in
cookies.
The cookie data is encrypted using IDataProtector, encoded with Base64UrlTextEncoder, then chunked. Because
the cookie is chunked, the single cookie size limit found in ASP.NET Core 1.x doesn't apply. The cookie data isn't
compressed because compressing encrypted data can lead to security problems such as the CRIME and BREACH
attacks. For more information on the cookie-based TempData provider, see CookieTempDataProvider.
In ASP.NET Core 1.0 and 1.1, the session state TempData provider is the default provider.
Choose a TempData provider
Choosing a TempData provider involves several considerations, such as:
1. Does the app already use session state? If so, using the session state TempData provider has no additional cost
to the app (aside from the size of the data).
2. Does the app use TempData only sparingly for relatively small amounts of data (up to 500 bytes)? If so, the
cookie TempData provider adds a small cost to each request that carries TempData. If not, the session state
TempData provider can be beneficial to avoid round-tripping a large amount of data in each request until the
TempData is consumed.
3. Does the app run in a server farm on multiple servers? If so, there's no additional configuration required to use
the cookie TempData provider outside of Data Protection (see Data Protection and Key storage providers).
NOTE
Most web clients (such as web browsers) enforce limits on the maximum size of each cookie, the total number of cookies, or
both. When using the cookie TempData provider, verify the app won't exceed these limits. Consider the total size of the
data. Account for increases in cookie size due to encryption and chunking.
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddSessionStateTempDataProvider();
services.AddSession();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.UseMvc();
}
The following Startup class code configures the session-based TempData provider:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSession();
}
The order of middleware is important. In the preceding example, an InvalidOperationException exception occurs
when UseSession is invoked after UseMvc . For more information, see Middleware Ordering.
IMPORTANT
If targeting .NET Framework and using the session-based TempData provider, add the Microsoft.AspNetCore.Session
package to the project.
Query strings
A limited amount of data can be passed from one request to another by adding it to the new request's query
string. This is useful for capturing state in a persistent manner that allows links with embedded state to be shared
through email or social networks. Because URL query strings are public, never use query strings for sensitive
data.
In addition to unintended sharing, including data in query strings can create opportunities for Cross-Site Request
Forgery (CSRF ) attacks, which can trick users into visiting malicious sites while authenticated. Attackers can then
steal user data from the app or take malicious actions on behalf of the user. Any preserved app or session state
must protect against CSRF attacks. For more information, see Prevent Cross-Site Request Forgery (XSRF/CSRF )
attacks.
Hidden fields
Data can be saved in hidden form fields and posted back on the next request. This is common in multi-page
forms. Because the client can potentially tamper with the data, the app must always revalidate the data stored in
hidden fields.
HttpContext.Items
The HttpContext.Items collection is used to store data while processing a single request. The collection's contents
are discarded after a request is processed. The Items collection is often used to allow components or middleware
to communicate when they operate at different points in time during a request and have no direct way to pass
parameters.
In the following example, middleware adds isVerified to the Items collection.
app.Use(async (context, next) =>
{
// perform some verification
context.Items["isVerified"] = true;
await next.Invoke();
});
Later in the pipeline, another middleware can access the value of isVerified :
For middleware that's only used by a single app, string keys are acceptable. Middleware shared between app
instances should use unique object keys to avoid key collisions. The following example shows how to use a unique
object key defined in a middleware class:
await _next(httpContext);
}
}
await _next(httpContext);
}
}
Other code can access the value stored in HttpContext.Items using the key exposed by the middleware class:
HttpContext.Items
.TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey,
out var middlewareSetValue);
SessionInfo_MiddlewareValue =
middlewareSetValue?.ToString() ?? "Middleware value not set!";
This approach also has the advantage of eliminating the use of key strings in the code.
Cache
Caching is an efficient way to store and retrieve data. The app can control the lifetime of cached items.
Cached data isn't associated with a specific request, user, or session. Be careful not to cache user-specific data
that may be retrieved by other users' requests.
For more information, see the Cache responses topic.
Dependency Injection
Use Dependency Injection to make data available to all users:
1. Define a service containing the data. For example, a class named MyAppData is defined:
public class MyAppData
{
// Declare properties and methods
}
Common errors
"Unable to resolve service for type 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while
attempting to activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'."
This is usually caused by failing to configure at least one IDistributedCache implementation. For more
information, see Work with a distributed cache and Cache in-memory.
In the event that the session middleware fails to persist a session (for example, if the backing store isn't
available), the middleware logs the exception and the request continues normally. This leads to
unpredictable behavior.
For example, a user stores a shopping cart in session. The user adds an item to the cart but the commit
fails. The app doesn't know about the failure so it reports to the user that the item was added to their cart,
which isn't true.
The recommended approach to check for errors is to call await feature.Session.CommitAsync(); from app
code when the app is done writing to the session. CommitAsync throws an exception if the backing store is
unavailable. If CommitAsync fails, the app can process the exception. LoadAsync throws under the same
conditions where the data store is unavailable.
Additional resources
Host ASP.NET Core in a web farm
File Providers in ASP.NET Core
8/1/2018 • 7 minutes to read • Edit Online
IMPLEMENTATION DESCRIPTION
PhysicalFileProvider
The PhysicalFileProvider provides access to the physical file system. PhysicalFileProvider uses the System.IO.File
type (for the physical provider) and scopes all paths to a directory and its children. This scoping prevents access to
the file system outside of the specified directory and its children. When instantiating this provider, a directory path
is required and serves as the base path for all requests made using the provider. You can instantiate a
PhysicalFileProvider provider directly, or you can request an IFileProvider in a constructor through dependency
injection.
Static types
The following code shows how to create a PhysicalFileProvider and use it to obtain directory contents and file
information:
The File Provider can be used to iterate through the directory specified by applicationRoot or call GetFileInfo to
obtain a file's information. The File Provider has no access outside of the applicationRoot directory.
The sample app creates the provider in the app's Startup.ConfigureServices class using
IHostingEnvironment.ContentRootFileProvider:
<ul>
@foreach (var item in Model.DirectoryContents)
{
if (item.IsDirectory)
{
<li><strong>@item.Name</strong></li>
}
else
{
<li>@item.Name - @item.Length bytes</li>
}
}
</ul>
In the sample app, the HomeController class receives an IFileProvider instance to obtain directory contents for
the app's base path.
Controllers/HomeController.cs:
return View(contents);
}
}
ManifestEmbeddedFileProvider
The ManifestEmbeddedFileProvider is used to access files embedded within assemblies. The
ManifestEmbeddedFileProvider uses a manifest compiled into the assembly to reconstruct the original paths of the
embedded files.
NOTE
The ManifestEmbeddedFileProvider is available in ASP.NET Core 2.1 or later. To access files embedded in assemblies in
ASP.NET Core 2.0 or earlier, see the ASP.NET Core 1.x version of this topic.
To generate a manifest of the embedded files, set the <GenerateEmbeddedFilesManifest> property to true . Specify
the files to embed with <EmbeddedResource>:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.txt" />
</ItemGroup>
</Project>
Use glob patterns to specify one or more files to embed into the assembly.
The sample app creates an ManifestEmbeddedFileProvider and passes the currently executing assembly to its
constructor.
Startup.cs:
var manifestEmbeddedProvider =
new ManifestEmbeddedFileProvider(Assembly.GetEntryAssembly());
OVERLOAD DESCRIPTION
ManifestEmbeddedFileProvider(Assembly, String) Accepts an optional root relative path parameter. Specify the
root to scope calls to GetDirectoryContents to those
resources under the provided path.
ManifestEmbeddedFileProvider(Assembly, String, String, Accepts an optional root relative path, lastModified date,
DateTimeOffset) and manifestName parameters. The manifestName
represents the name of the embedded resource containing
the manifest.
EmbeddedFileProvider
The EmbeddedFileProvider is used to access files embedded within assemblies. Specify the files to embed with the
<EmbeddedResource> property in the project file:
<ItemGroup>
<EmbeddedResource Include="Resource.txt" />
</ItemGroup>
Use glob patterns to specify one or more files to embed into the assembly.
The sample app creates an EmbeddedFileProvider and passes the currently executing assembly to its constructor.
Startup.cs:
Embedded resources don't expose directories. Rather, the path to the resource (via its namespace) is embedded in
its filename using . separators. In the sample app, the baseNamespace is FileProviderSample. .
The EmbeddedFileProvider(Assembly, String) constructor accepts an optional baseNamespace parameter. Specify
the base namespace to scope calls to GetDirectoryContents to those resources under the provided namespace.
CompositeFileProvider
The CompositeFileProvider combines IFileProvider instances, exposing a single interface for working with files
from multiple providers. When creating the CompositeFileProvider , pass one or more IFileProvider instances to
its constructor.
In the sample app, a PhysicalFileProvider and a ManifestEmbeddedFileProvider provide files to a
CompositeFileProvider registered in the app's service container:
services.AddSingleton<IFileProvider>(compositeProvider);
In the sample app, a PhysicalFileProvider and an EmbeddedFileProvider provide files to a CompositeFileProvider
registered in the app's service container:
services.AddSingleton<IFileProvider>(compositeProvider);
In the sample app, the WatchConsole console app is configured to display a message whenever a text file is
modified:
while (true)
{
MainAsync().GetAwaiter().GetResult();
}
}
token.RegisterChangeCallback(state =>
((TaskCompletionSource<object>)state).TrySetResult(null), tcs);
await tcs.Task.ConfigureAwait(false);
Console.WriteLine("quotes.txt changed");
}
private static PhysicalFileProvider _fileProvider =
new PhysicalFileProvider(Directory.GetCurrentDirectory());
while (true)
{
MainAsync().GetAwaiter().GetResult();
}
}
token.RegisterChangeCallback(state =>
((TaskCompletionSource<object>)state).TrySetResult(null), tcs);
await tcs.Task.ConfigureAwait(false);
Console.WriteLine("quotes.txt changed");
}
Some file systems, such as Docker containers and network shares, may not reliably send change notifications. Set
the DOTNET_USE_POLLING_FILE_WATCHER environment variable to 1 or true to poll the file system for changes every
four seconds (not configurable).
Glob patterns
File system paths use wildcard patterns called glob (or globbing ) patterns. Specify groups of files with these
patterns. The two wildcard characters are * and ** :
*
Matches anything at the current folder level, any filename, or any file extension. Matches are terminated by / and
. characters in the file path.
**
Matches anything across multiple directory levels. Can be used to recursively match many files within a directory
hierarchy.
Glob pattern examples
directory/file.txt
Matches a specific file in a specific directory.
directory/*.txt
Matches all files with .txt extension in a specific directory.
directory/*/appsettings.json
Matches all appsettings.json files in directories exactly one level below the directory folder.
directory/**/*.txt
Matches all files with .txt extension found anywhere under the directory folder.
Repository pattern with ASP.NET Core
7/30/2018 • 3 minutes to read • Edit Online
Repository interface
A repository interface defines the properties and methods for implementation. In the sample app, the repository
interface for movie character data is ICharacterRepository . ICharacterRepository defines the ListAll and Add
methods required to work with Character instances in the app:
[Key]
public Guid Id { get; private set; } = Guid.NewGuid();
public string Name { get; private set; } = String.Empty;
}
private Character()
{
}
[Key]
public Guid Id { get; private set; } = Guid.NewGuid();
public string Name { get; private set; } = String.Empty;
}
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Characters = _characterRepository.ListAll();
}
return View(characters);
}
Additional resources
DevIQ: Repository Pattern
Dependency injection
Dependency injection into views
Dependency injection into controllers
Dependency injection in requirement handlers
Inversion of Control Containers and the Dependency Injection Pattern
Globalization and localization in ASP.NET Core
8/1/2018 • 16 minutes to read • Edit Online
By Rick Anderson, Damien Bowden, Bart Calixto, Nadeem Afana, and Hisham Bin Ateya
Creating a multilingual website with ASP.NET Core will allow your site to reach a wider audience. ASP.NET
Core provides services and middleware for localizing into different languages and cultures.
Internationalization involves Globalization and Localization. Globalization is the process of designing apps that
support different cultures. Globalization adds support for input, display, and output of a defined set of
language scripts that relate to specific geographic areas.
Localization is the process of adapting a globalized app, which you have already processed for localizability, to
a particular culture/locale. For more information see Globalization and localization terms near the end of
this document.
App localization involves the following:
1. Make the app's content localizable
2. Provide localized resources for the languages and cultures you support
3. Implement a strategy to select the language/culture for each request
View or download sample code (how to download)
namespace Localization.StarterWeb.Controllers
{
[Route("api/[controller]")]
public class AboutController : Controller
{
private readonly IStringLocalizer<AboutController> _localizer;
[HttpGet]
public string Get()
{
return _localizer["About Title"];
}
}
}
In the code above, the IStringLocalizer<T> implementation comes from Dependency Injection. If the localized
value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title". You can leave
the default language literal strings in the app and wrap them in the localizer, so that you can focus on
developing the app. You develop your app with your default language and prepare it for the localization step
without first creating a default resource file. Alternatively, you can use the traditional approach and provide a
key to retrieve the default language string. For many developers the new workflow of not having a default
language .resx file and simply wrapping the string literals can reduce the overhead of localizing an app. Other
developers will prefer the traditional work flow as it can make it easier to work with longer string literals and
make it easier to update localized strings.
Use the IHtmlLocalizer<T> implementation for resources that contain HTML. IHtmlLocalizer HTML encodes
arguments that are formatted in the resource string, but doesn't HTML encode the resource string itself. In the
sample highlighted below, only the value of name parameter is HTML encoded.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
namespace Localization.StarterWeb.Controllers
{
public class BookController : Controller
{
private readonly IHtmlLocalizer<BookController> _localizer;
return View();
}
Note: You generally want to only localize text and not HTML.
At the lowest level, you can get IStringLocalizerFactory out of Dependency Injection:
{
public class TestController : Controller
{
private readonly IStringLocalizer _localizer;
private readonly IStringLocalizer _localizer2;
The code above demonstrates each of the two factory create methods.
You can partition your localized strings by controller, area, or have just one container. In the sample app, a
dummy class named SharedResource is used for shared resources.
namespace Localization.StarterWeb
{
public class SharedResource
{
}
}
Some developers use the Startup class to contain global or shared strings. In the sample below, the
InfoController and the SharedResource localizers are used:
View localization
The IViewLocalizer service provides localized strings for a view. The ViewLocalizer class implements this
interface and finds the resource location from the view file path. The following code shows how to use the
default implementation of IViewLocalizer :
@using Microsoft.AspNetCore.Mvc.Localization
@{
ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
The default implementation of IViewLocalizer finds the resource file based on the view's file name. There's no
option to use a global shared resource file. ViewLocalizer implements the localizer using IHtmlLocalizer , so
Razor doesn't HTML encode the localized string. You can parameterize resource strings and IViewLocalizer
will HTML encode the parameters, but not the resource string. Consider the following Razor markup:
KEY VALUE
The rendered view would contain the HTML markup from the resource file.
Note: You generally want to only localize text and not HTML.
To use a shared resource file in a view, inject IHtmlLocalizer<T> :
@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.StarterWeb.Services
@{
ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h1>@SharedLocalizer["Hello!"]</h1>
DataAnnotations localization
DataAnnotations error messages are localized with IStringLocalizer<T> . Using the option
ResourcesPath = "Resources" , the error messages in RegisterViewModel can be stored in either of the following
paths:
Resources/ViewModels.Account.RegisterViewModel.fr.resx
Resources/ViewModels/Account/RegisterViewModel.fr.resx
public class RegisterViewModel
{
[Required(ErrorMessage = "The Email field is required.")]
[EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
[Display(Name = "Email")]
public string Email { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
In ASP.NET Core MVC 1.1.0 and higher, non-validation attributes are localized. ASP.NET Core MVC 1.0 does
not look up localized strings for non-validation attributes.
Using one resource string for multiple classes
The following code shows how to use one resource string for validation attributes with multiple classes:
In the preceeding code, SharedResource is the class corresponding to the resx where your validation messages
are stored. With this approach, DataAnnotations will only use SharedResource , rather than the resource for
each class.
Resource files
A resource file is a useful mechanism for separating localizable strings from code. Translated strings for the
non-default language are isolated .resx resource files. For example, you might want to create Spanish resource
file named Welcome.es.resx containing translated strings. "es" is the language code for Spanish. To create this
resource file in Visual Studio:
1. In Solution Explorer, right click on the folder which will contain the resource file > Add > New Item.
2. In the Search installed templates box, enter "resource" and name the file.
3. Enter the key value (native string) in the Name column and the translated string in the Value column.
Resources/Controllers.HomeController.fr.resx Dot
Resources/Controllers/HomeController.fr.resx Path
Resource files using @inject IViewLocalizer in Razor views follow a similar pattern. The resource file for a
view can be named using either dot naming or path naming. Razor view resource files mimic the path of their
associated view file. Assuming we set the ResourcesPath to "Resources", the French resource file associated
with the Views/Home/About.cshtml view could be either of the following:
Resources/Views/Home/About.fr.resx
Resources/Views.Home.About.fr.resx
If you don't use the ResourcesPath option, the .resx file for a view would be located in the same folder as the
view.
RootNamespaceAttribute
The RootNamespace attribute provides the root namespace of an assembly when the root namespace of an
assembly is different than the assembly name.
If the root namespace of an assembly is different than the assembly name:
Localization does not work by default.
Localization fails due to the way resources are searched for within the assembly. RootNamespace is a build-
time value which is not available to the executing process.
If the RootNamespace is different from the AssemblyName , include the following in AssemblyInfo.cs (with
parameter values replaced with the actual values):
using System.Reflection;
using Microsoft.Extensions.Localization;
As an example, if you remove the ".fr" culture designator and you have the culture set to French, the default
resource file is read and strings are localized. The Resource manager designates a default or fallback resource
for when nothing meets your requested culture. If you want to just return the key when missing a resource for
the requested culture you must not have a default resource file.
Generate resource files with Visual Studio
If you create a resource file in Visual Studio without a culture in the file name (for example, Welcome.resx),
Visual Studio will create a C# class with a property for each string. That's usually not what you want with
ASP.NET Core. You typically don't have a default .resx resource file (a .resx file without the culture name). We
suggest you create the .resx file with a culture name (for example Welcome.fr.resx). When you create a .resx file
with a culture name, Visual Studio won't generate the class file. We anticipate that many developers won't
create a default language resource file.
Add other cultures
Each language and culture combination (other than the default language) requires a unique resource file. You
create resource files for different cultures and locales by creating new resource files in which the ISO language
codes are part of the file name (for example, en-us, fr-ca, and en-gb). These ISO codes are placed between the
file name and the .resx file extension, as in Welcome.es-MX.resx (Spanish/Mexico).
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
AddLocalization Adds the localization services to the services container. The code above also sets the
resources path to "Resources".
AddViewLocalization Adds support for localized view files. In this sample view localization is based on
the view file suffix. For example "fr" in the Index.fr.cshtml file.
AddDataAnnotationsLocalization Adds support for localized DataAnnotations validation messages
through IStringLocalizer abstractions.
Localization middleware
The current culture on a request is set in the localization Middleware. The localization middleware is enabled in
the Configure method. The localization middleware must be configured before any middleware which might
check the request culture (for example, app.UseMvcWithDefaultRoute() ).
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(enUSCulture),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// UI strings that we have localized.
SupportedUICultures = supportedCultures
});
app.UseStaticFiles();
// To configure external authentication,
// see: http://go.microsoft.com/fwlink/?LinkID=532715
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
The default list goes from most specific to least specific. Later in the article we'll see how you can change the
order and even add a custom culture provider. If none of the providers can determine the request culture, the
DefaultRequestCulture is used.
QueryStringRequestCultureProvider
Some apps will use a query string to set the culture and UI culture. For apps that use the cookie or Accept-
Language header approach, adding a query string to the URL is useful for debugging and testing code. By
default, the QueryStringRequestCultureProvider is registered as the first localization provider in the
RequestCultureProvider list. You pass the query string parameters culture and ui-culture . The following
example sets the specific culture (language and region) to Spanish/Mexico:
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
If you only pass in one of the two ( culture or ui-culture ), the query string provider will set both values using
the one you passed in. For example, setting just the culture will set both the Culture and the UICulture :
http://localhost:5000/?culture=es-MX
CookieRequestCultureProvider
Production apps will often provide a mechanism to set the culture with the ASP.NET Core culture cookie. Use
the MakeCookieValue method to create a cookie.
The CookieRequestCultureProvider DefaultCookieName returns the default cookie name used to track the user's
preferred culture information. The default cookie name is .AspNetCore.Culture .
The cookie format is c=%LANGCODE%|uic=%LANGCODE% , where c is Culture and uic is UICulture , for example:
c=en-UK|uic=en-US
If you only specify one of culture info and UI culture, the specified culture will be used for both culture info and
UI culture.
The Accept-Language HTTP header
The Accept-Language header is settable in most browsers and was originally intended to specify the user's
language. This setting indicates what the browser has been set to send or has inherited from the underlying
operating system. The Accept-Language HTTP header from a browser request isn't an infallible way to detect
the user's preferred language (see Setting language preferences in a browser). A production app should
include a way for a user to customize their choice of culture.
Set the Accept-Language HTTP header in IE
1. From the gear icon, tap Internet Options.
2. Tap Languages.
3. Tap Set Language Preferences.
4. Tap Add a language.
5. Add the language.
6. Tap the language, then tap Move Up.
Use a custom provider
Suppose you want to let your customers store their language and culture in your databases. You could write a
provider to look up these values for the user. The following code shows how to add a custom provider:
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo(enUSCulture),
new CultureInfo("fr")
};
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options
@{
var requestCulture = Context.Features.Get<IRequestCultureFeature>();
var cultureItems = LocOptions.Value.SupportedUICultures
.Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
.ToList();
var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}
The Views/Shared/_SelectLanguagePartial.cshtml file is added to the footer section of the layout file so it will
be available to all views:
return LocalRedirect(returnUrl);
}
You can't plug in the _SelectLanguagePartial.cshtml to sample code for this project. The
Localization.StarterWeb project on GitHub has code to flow the RequestLocalizationOptions to a Razor
partial through the Dependency Injection container.
Additional resources
Localization.StarterWeb project used in the article.
Resource Files in Visual Studio
Resources in .resx Files
Microsoft Multilingual App Toolkit
Configure portable object localization in ASP.NET
Core
6/21/2018 • 6 minutes to read • Edit Online
What is a PO file?
PO files are distributed as text files containing the translated strings for a given language. Some advantages of
using PO files instead .resx files include:
PO files support pluralization; .resx files don't support pluralization.
PO files aren't compiled like .resx files. As such, specialized tooling and build steps aren't required.
PO files work well with collaborative online editing tools.
Example
Here is a sample PO file containing the translation for two strings in French, including one with its plural form:
fr.po
#: Services/EmailService.cs:29
msgid "Enter a comma separated list of email addresses."
msgstr "Entrez une liste d'emails séparés par une virgule."
#: Views/Email.cshtml:112
msgid "The email address is \"{0}\"."
msgid_plural "The email addresses are \"{0}\"."
msgstr[0] "L'adresse email est \"{0}\"."
msgstr[1] "Les adresses email sont \"{0}\""
services.AddPortableObjectLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("en"),
new CultureInfo("fr-FR"),
new CultureInfo("fr")
};
app.UseStaticFiles();
app.UseRequestLocalization();
app.UseMvcWithDefaultRoute();
}
Add the following code to your Razor view of choice. About.cshtml is used in this example.
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
<p>@Localizer["Hello world!"]</p>
An IViewLocalizer instance is injected and used to translate the text "Hello world!".
Creating a PO file
Create a file named .po in your application root folder. In this example, the file name is fr.po because the French
language is used:
This file stores both the string to translate and the French-translated string. Translations revert to their parent
culture, if necessary. In this example, the fr.po file is used if the requested culture is fr-FR or fr-CA .
Testing the application
Run your application, and navigate to the URL /Home/About . The text Hello world! is displayed.
Navigate to the URL /Home/About?culture=fr-FR . The text Bonjour le monde! is displayed.
Pluralization
PO files support pluralization forms, which is useful when the same string needs to be translated differently based
on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which
string to use based on the cardinality.
The Orchard Localization package provides an API to invoke these different plural forms automatically.
Creating pluralization PO files
Add the following content to the previously mentioned fr.po file:
See What is a PO file? for an explanation of what each entry in this example represents.
Adding a language using different pluralization forms
English and French strings were used in the previous example. English and French have only two pluralization
forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other
cardinality is mapped to the second plural form.
Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.
Create the cs.po file as follows, and note how the pluralization needs three different translations:
msgid "Hello world!"
msgstr "Ahoj světe!!"
To accept Czech localizations, add "cs" to the list of supported cultures in the ConfigureServices method:
Edit the Views/Home/About.cshtml file to render localized, plural strings for several cardinalities:
Note: In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code
with three different values to expose a very specific case.
Upon switching cultures, you see the following:
For /Home/About :
For /Home/About?culture=fr :
Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.
For /Home/About?culture=cs :
Note that for the Czech culture, the three translations are different. The French and English cultures share the same
construction for the two last translated strings.
Advanced tasks
Contextualizing strings
Applications often contain the strings to be translated in several places. The same string may have a different
translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file
context, which can be used to categorize the string being represented. Using a file context, a string can be translated
differently, depending on the file context (or lack of a file context).
The PO localization services use the name of the full class or the view that's used when translating a string. This is
accomplished by setting the value on the msgctxt entry.
Consider a minor addition to the previous fr.po example. A Razor view located at Views/Home/About.cshtml can be
defined as the file context by setting the reserved msgctxt entry's value:
msgctxt "Views.Home.About"
msgid "Hello world!"
msgstr "Bonjour le monde!"
With the msgctxt set as such, text translation occurs when navigating to /Home/About?culture=fr-FR . The
translation won't occur when navigating to /Home/Contact?culture=fr-FR .
When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an
appropriate PO file without a context. Assuming there's no specific file context defined for
Views/Home/Contact.cshtml, navigating to /Home/Contact?culture=fr-FR loads a PO file such as:
In this example, the PO files are loaded from the Localization folder.
Implementing a custom logic for finding localization files
When more complex logic is needed to locate PO files, the
OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider interface can be implemented and
registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be
found within a hierarchy of folders.
Using a different default pluralized language
The package includes a Plural extension method that's specific to two plural forms. For languages requiring more
plural forms, create an extension method. With an extension method, you won't need to provide any localization file
for the default language — the original strings are already available directly in the code.
You can use the more generic Plural(int count, string[] pluralForms, params object[] arguments) overload which
accepts a string array of translations.
Initiate HTTP requests
8/20/2018 • 12 minutes to read • Edit Online
Prerequisites
Projects targeting .NET Framework require installation of the Microsoft.Extensions.Http NuGet package. Projects
that target .NET Core and reference the Microsoft.AspNetCore.App metapackage already include the
Microsoft.Extensions.Http package.
Consumption patterns
There are several ways IHttpClientFactory can be used in an app:
Basic usage
Named clients
Typed clients
Generated clients
None of them are strictly superior to another. The best approach depends upon the app's constraints.
Basic usage
The IHttpClientFactory can be registered by calling the AddHttpClient extension method on the
IServiceCollection , inside the Startup.ConfigureServices method.
services.AddHttpClient();
Once registered, code can accept an IHttpClientFactory anywhere services can be injected with dependency
injection (DI). The IHttpClientFactory can be used to create a HttpClient instance:
public class BasicUsageModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
if (response.IsSuccessStatusCode)
{
Branches = await response.Content
.ReadAsAsync<IEnumerable<GitHubBranch>>();
}
else
{
GetBranchesError = true;
Branches = Array.Empty<GitHubBranch>();
}
}
}
Using IHttpClientFactory in this fashion is a great way to refactor an existing app. It has no impact on the way
HttpClient is used. In places where HttpClient instances are currently created, replace those occurrences with a
call to CreateClient.
Named clients
If an app requires many distinct uses of HttpClient , each with a different configuration, an option is to use
named clients. Configuration for a named HttpClient can be specified during registration in
Startup.ConfigureServices .
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Github API versioning
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
// Github requires a user-agent
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
In the preceding code, AddHttpClient is called, providing the name github. This client has some default
configuration applied—namely the base address and two headers required to work with the GitHub API.
Each time CreateClient is called, a new instance of HttpClient is created and the configuration action is called.
To consume a named client, a string parameter can be passed to CreateClient . Specify the name of the client to be
created:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
if (response.IsSuccessStatusCode)
{
PullRequests = await response.Content
.ReadAsAsync<IEnumerable<GitHubPullRequest>>();
}
else
{
GetPullRequestsError = true;
PullRequests = Array.Empty<GitHubPullRequest>();
}
}
}
In the preceding code, the request doesn't need to specify a hostname. It can pass just the path, since the base
address configured for the client is used.
Typed clients
Typed clients provide the same capabilities as named clients without the need to use strings as keys. The typed
client approach provides IntelliSense and compiler help when consuming clients. They provide a single location to
configure and interact with a particular HttpClient . For example, a single typed client might be used for a single
backend endpoint and encapsulate all logic dealing with that endpoint. Another advantage is that they work with
DI and can be injected where required in your app.
A typed client accepts a HttpClient parameter in its constructor:
public class GitHubService
{
public HttpClient Client { get; }
Client = client;
}
response.EnsureSuccessStatusCode();
return result;
}
}
In the preceding code, the configuration is moved into the typed client. The HttpClient object is exposed as a
public property. It's possible to define API-specific methods that expose HttpClient functionality. The
GetAspNetDocsIssues method encapsulates the code needed to query for and parse out the latest open issues from
a GitHub repository.
To register a typed client, the generic AddHttpClient extension method can be used within
Startup.ConfigureServices , specifying the typed client class:
services.AddHttpClient<GitHubService>();
The typed client is registered as transient with DI. The typed client can be injected and consumed directly:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
If preferred, the configuration for a typed client can be specified during registration in Startup.ConfigureServices ,
rather than in the typed client's constructor:
services.AddHttpClient<RepoService>(c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
It's possible to entirely encapsulate the HttpClient within a typed client. Rather than exposing it as a property,
public methods can be provided which call the HttpClient instance internally.
public class RepoService
{
// _httpClient isn't exposed publicly
private readonly HttpClient _httpClient;
response.EnsureSuccessStatusCode();
return result;
}
}
In the preceding code, the HttpClient is stored as a private field. All access to make external calls goes through
the GetRepos method.
Generated clients
IHttpClientFactory can be used in combination with other third-party libraries such as Refit. Refit is a REST
library for .NET. It converts REST APIs into live interfaces. An implementation of the interface is generated
dynamically by the RestService , using HttpClient to make the external HTTP calls.
An interface and a reply are defined to represent the external API and its response:
services.AddMvc();
}
The defined interface can be consumed where necessary, with the implementation provided by DI and Refit:
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IHelloClient _client;
[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}
The preceding code defines a basic handler. It checks to see if an X-API-KEY header has been included on the
request. If the header is missing, it can avoid the HTTP call and return a suitable response.
During registration, one or more handlers can be added to the configuration for a HttpClient . This task is
accomplished via extension methods on the IHttpClientBuilder.
services.AddTransient<ValidateHeaderHandler>();
services.AddHttpClient("externalservice", c =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
In the preceding code, the ValidateHeaderHandler is registered with DI. The handler must be registered in DI as
transient. Once registered, AddHttpMessageHandler can be called, passing in the type for the handler.
Multiple handlers can be registered in the order that they should execute. Each handler wraps the next handler
until the final HttpClientHandler executes the request:
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();
services.AddHttpClient("clientwithhandlers")
// This handler is on the outside and called first during the
// request, last during the response.
.AddHttpMessageHandler<SecureRequestHandler>()
// This handler is on the inside, closest to the request being
// sent.
.AddHttpMessageHandler<RequestDataHandler>();
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.1" />
</ItemGroup>
</Project>
After restoring this package, extension methods are available to support adding Polly-based handlers to clients.
Handle transient faults
Most common faults occur when external HTTP calls are transient. A convenient extension method called
AddTransientHttpErrorPolicy is included which allows a policy to be defined to handle transient errors. Policies
configured with this extension method handle HttpRequestException , HTTP 5xx responses, and HTTP 408
responses.
The AddTransientHttpErrorPolicy extension can be used within Startup.ConfigureServices . The extension provides
access to a PolicyBuilder object configured to handle errors representing a possible transient fault:
services.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
In the preceding code, a WaitAndRetryAsync policy is defined. Failed requests are retried up to three times with a
delay of 600 ms between attempts.
Dynamically select policies
Additional extension methods exist which can be used to add Polly-based handlers. One such extension is
AddPolicyHandler , which has multiple overloads. One overload allows the request to be inspected when defining
which policy to apply:
services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
.AddPolicyHandler(request =>
request.Method == HttpMethod.Get ? timeout : longTimeout);
In the preceding code, if the outgoing request is a GET, a 10-second timeout is applied. For any other HTTP
method, a 30-second timeout is used.
Add multiple Polly handlers
It is common to nest Polly policies to provide enhanced functionality:
services.AddHttpClient("multiplepolicies")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
.AddTransientHttpErrorPolicy(
p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
In the preceding example, two handlers are added. The first uses the AddTransientHttpErrorPolicy extension to add
a retry policy. Failed requests are retried up to three times. The second call to AddTransientHttpErrorPolicy adds a
circuit breaker policy. Further external requests are blocked for 30 seconds if five failed attempts occur
sequentially. Circuit breaker policies are stateful. All calls through this client share the same circuit state.
Add policies from the Polly registry
An approach to managing regularly used policies is to define them once and register them with a PolicyRegistry .
An extension method is provided which allows a handler to be added using a policy from the registry:
registry.Add("regular", timeout);
registry.Add("long", longTimeout);
services.AddHttpClient("regulartimeouthandler")
.AddPolicyHandlerFromRegistry("regular");
In the preceding code, two policies are registered when the PolicyRegistry is added to the ServiceCollection . To
use a policy from the registry, the AddPolicyHandlerFromRegistry method is used, passing the name of the policy to
apply.
Further information about IHttpClientFactory and Polly integrations can be found on the Polly wiki.
services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Disposal of the client isn't required. Disposal cancels outgoing requests and guarantees the given HttpClient
instance can't be used after calling Dispose. IHttpClientFactory tracks and disposes resources used by
HttpClient instances. The HttpClient instances can generally be treated as .NET objects not requiring disposal.
Keeping a single HttpClient instance alive for a long duration is a common pattern used before the inception of
IHttpClientFactory . This pattern becomes unnecessary after migrating to IHttpClientFactory .
Logging
Clients created via IHttpClientFactory record log messages for all requests. Enable the appropriate information
level in your logging configuration to see the default log messages. Additional logging, such as the logging of
request headers, is only included at trace level.
The log category used for each client includes the name of the client. A client named MyNamedClient, for example,
logs messages with a category of System.Net.Http.HttpClient.MyNamedClient.LogicalHandler . Messages suffixed
with LogicalHandler occur outside the request handler pipeline. On the request, messages are logged before any
other handlers in the pipeline have processed it. On the response, messages are logged after any other pipeline
handlers have received the response.
Logging also occurs inside the request handler pipeline. In the MyNamedClient example, those messages are
logged against the log category System.Net.Http.HttpClient.MyNamedClient.ClientHandler . For the request, this
occurs after all other handlers have run and immediately before the request is sent out on the network. On the
response, this logging includes the state of the response before it passes back through the handler pipeline.
Enabling logging outside and inside the pipeline enables inspection of the changes made by the other pipeline
handlers. This may include changes to request headers, for example, or to the response status code.
Including the name of the client in the log category enables log filtering for specific named clients where
necessary.
services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
Request Features in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
By Steve Smith
Web server implementation details related to HTTP requests and responses are defined in interfaces. These
interfaces are used by server implementations and middleware to create and modify the application's hosting
pipeline.
Feature interfaces
ASP.NET Core defines a number of HTTP feature interfaces in Microsoft.AspNetCore.Http.Features which are
used by servers to identify the features they support. The following feature interfaces handle requests and return
responses:
IHttpRequestFeature Defines the structure of an HTTP request, including the protocol, path, query string, headers,
and body.
IHttpResponseFeature Defines the structure of an HTTP response, including the status code, headers, and body of
the response.
IHttpAuthenticationFeature Defines support for identifying users based on a ClaimsPrincipal and specifying an
authentication handler.
IHttpUpgradeFeature Defines support for HTTP Upgrades, which allow the client to specify which additional
protocols it would like to use if the server wishes to switch protocols.
IHttpBufferingFeature Defines methods for disabling buffering of requests and/or responses.
IHttpConnectionFeature Defines properties for local and remote addresses and ports.
IHttpRequestLifetimeFeature Defines support for aborting connections, or detecting if a request has been
terminated prematurely, such as by a client disconnect.
IHttpSendFileFeature Defines a method for sending files asynchronously.
IHttpWebSocketFeature Defines an API for supporting web sockets.
IHttpRequestIdentifierFeature Adds a property that can be implemented to uniquely identify requests.
ISessionFeature Defines ISessionFactory and ISession abstractions for supporting user sessions.
ITlsConnectionFeature Defines an API for retrieving client certificates.
ITlsTokenBindingFeature Defines methods for working with TLS token binding parameters.
NOTE
ISessionFeature isn't a server feature, but is implemented by the SessionMiddleware (see Managing Application State).
Feature collections
The Features property of HttpContext provides an interface for getting and setting the available HTTP features
for the current request. Since the feature collection is mutable even within the context of a request, middleware
can be used to modify the collection and add support for additional features.
Summary
Feature interfaces define specific HTTP features that a given request may support. Servers define collections of
features, and the initial set of features supported by that server, but middleware can be used to enhance these
features.
Additional resources
Servers
Middleware
Open Web Interface for .NET (OWIN )
Access HttpContext in ASP.NET Core
7/28/2018 • 2 minutes to read • Edit Online
ASP.NET Core apps access the HttpContext through the IHttpContextAccessor interface and its default
implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor when you need access to
the HttpContext inside a service.
@{
var username = Context.User.Identity.Name;
}
return View();
}
}
ASP.NET Core primitives are low -level building blocks shared by framework extensions. You can use these
building blocks in your own code.
Detect changes with Change Tokens
Detect changes with change tokens in ASP.NET Core
8/25/2018 • 8 minutes to read • Edit Online
By Luke Latham
A change token is a general-purpose, low -level building block used to track changes.
View or download sample code (how to download)
IChangeToken interface
IChangeToken propagates notifications that a change has occurred. IChangeToken resides in the
Microsoft.Extensions.Primitives namespace. For apps that don't use the Microsoft.AspNetCore.App metapackage
(ASP.NET Core 2.1 or later), reference the Microsoft.Extensions.Primitives NuGet package in the project file.
IChangeToken has two properties:
ActiveChangedCallbacks indicate if the token proactively raises callbacks. If ActiveChangedCallbacks is set to
false , a callback is never called, and the app must poll HasChanged for changes. It's also possible for a token
to never be cancelled if no changes occur or the underlying change listener is disposed or disabled.
HasChanged gets a value that indicates if a change has occurred.
The interface has one method, RegisterChangeCallback(Action<Object>, Object), which registers a callback that's
invoked when the token has changed. HasChanged must be set before the callback is invoked.
ChangeToken class
ChangeToken is a static class used to propagate notifications that a change has occurred. ChangeToken resides in
the Microsoft.Extensions.Primitives namespace. For apps that don't use the Microsoft.AspNetCore.App
metapackage, reference the Microsoft.Extensions.Primitives NuGet package in the project file.
The ChangeToken OnChange(Func<IChangeToken>, Action) method registers an Action to call whenever the
token changes:
Func<IChangeToken> produces the token.
Action is called when the token changes.
ChangeToken has an OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload that takes an
additional TState parameter that's passed into the token consumer Action .
OnChange returns an IDisposable. Calling Dispose stops the token from listening for further changes and releases
the token's resources.
while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fs = File.OpenRead(filePath))
{
return System.Security.Cryptography.SHA1.Create().ComputeHash(fs);
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3 || ex.HResult != -2147024864)
{
throw;
}
else
{
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
}
A retry is implemented with an exponential back-off. The re-try is present because file locking may occur that
temporarily prevents computing a new hash on one of the files.
Simple startup change token
Register a token consumer Action callback for change notifications to the configuration reload token (Startup.cs):
ChangeToken.OnChange(
() => config.GetReloadToken(),
(state) => InvokeChanged(state),
env);
if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;
The state of the callback is used to pass in the IHostingEnvironment . This is useful to determine the correct
appsettings configuration JSON file to monitor, appsettings.<Environment>.json. File hashes are used to prevent
the WriteConsole statement from running multiple times due to multiple token callbacks when the configuration
file has only changed once.
This system runs as long as the app is running and can't be disabled by the user.
Monitoring configuration changes as a service
The sample implements:
Basic startup token monitoring.
Monitoring as a service.
A mechanism to enable and disable monitoring.
The sample establishes an IConfigurationMonitor interface (Extensions/ConfigurationMonitor.cs):
The constructor of the implemented class, ConfigurationMonitor , registers a callback for change notifications:
ChangeToken.OnChange<IConfigurationMonitor>(
() => config.GetReloadToken(),
InvokeChanged,
this);
}
config.GetReloadToken() supplies the token. InvokeChanged is the callback method. The state in this instance is a
reference to the IConfigurationMonitor instance that is used to access the monitoring state. Two properties are
used:
MonitoringEnabled indicates if the callback should run its custom code.
CurrentState describes the current monitoring state for use in the UI.
The InvokeChanged method is similar to the earlier approach, except that it:
Doesn't run its code unless MonitoringEnabled is true .
Notes the current state in its WriteConsole output.
if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
string message = $"State updated at {DateTime.Now}";
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;
services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();
The Index page offers the user control over configuration monitoring. The instance of IConfigurationMonitor is
injected into the IndexModel :
public IndexModel(
IConfiguration config,
IConfigurationMonitor monitor,
FileService fileService)
{
_config = config;
_monitor = monitor;
_fileService = fileService;
}
return RedirectToPage();
}
return RedirectToPage();
}
When OnPostStartMonitoring is triggered, monitoring is enabled, and the current state is cleared. When
OnPostStopMonitoring is triggered, monitoring is disabled, and the state is set to reflect that monitoring isn't
occurring.
while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fileStreamReader = File.OpenText(filePath))
{
return await fileStreamReader.ReadToEndAsync();
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3 || ex.HResult != -2147024864)
{
throw;
}
else
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
}
return null;
}
A FileService is created to handle cached file lookups. The GetFileContent method call of the service attempts to
obtain file content from the in-memory cache and return it to the caller (Services/FileService.cs).
If cached content isn't found using the cache key, the following actions are taken:
1. The file content is obtained using GetFileContent .
2. A change token is obtained from the file provider with IFileProviders.Watch. The token's callback is triggered
when the file is modified.
3. The file content is cached with a sliding expiration period. The change token is attached with
MemoryCacheEntryExtensions.AddExpirationToken to evict the cache entry if the file changes while it's cached.
public class FileService
{
private readonly IMemoryCache _cache;
private readonly IFileProvider _fileProvider;
private List<string> _tokens = new List<string>();
if (fileContent != null)
{
// Obtain a change token from the file provider whose
// callback is triggered when the file is modified.
var changeToken = _fileProvider.Watch(fileName);
return fileContent;
}
return string.Empty;
}
}
The FileService is registered in the service container along with the memory caching service ( Startup.cs):
services.AddMemoryCache();
services.AddSingleton<FileService>();
The page model loads the file's content using the service (Pages/Index.cshtml.cs):
var compositeChangeToken =
new CompositeChangeToken(
new List<IChangeToken>
{
firstCancellationChangeToken,
secondCancellationChangeToken
});
HasChanged on the composite token reports true if any represented token HasChanged is true .
ActiveChangeCallbacks on the composite token reports true if any represented token ActiveChangeCallbacks is
true . If multiple concurrent change events occur, the composite change callback is invoked exactly one time.
Additional resources
Cache in-memory
Work with a distributed cache
Response caching
Response Caching Middleware
Cache Tag Helper
Distributed Cache Tag Helper
Open Web Interface for .NET (OWIN) with ASP.NET
Core
8/16/2018 • 5 minutes to read • Edit Online
NOTE
Using these adapters comes with a performance cost. Apps using only ASP.NET Core components shouldn't use the
Microsoft.AspNetCore.Owin package or adapters.
You can configure other actions to take place within the OWIN pipeline.
NOTE
Response headers should only be modified prior to the first write to the response stream.
NOTE
Multiple calls to UseOwin is discouraged for performance reasons. OWIN components will operate best if grouped
together.
app.UseOwin(pipeline =>
{
pipeline(async (next) =>
{
// do something before
await OwinHello(new OwinEnvironment(HttpContext));
// do something after
});
});
namespace NowinSample
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseNowin()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
We'll also add an IWebHostBuilder extension to make it easy to add and configure the Nowin server.
using System;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.DependencyInjection;
using Nowin;
using NowinSample;
namespace Microsoft.AspNetCore.Hosting
{
public static class NowinWebHostBuilderExtensions
{
public static IWebHostBuilder UseNowin(this IWebHostBuilder builder)
{
return builder.ConfigureServices(services =>
{
services.AddSingleton<IServer, NowinServer>();
});
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
namespace NowinSample
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseNowin()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
app.Run(context =>
{
return context.Response.WriteAsync("Hello World");
});
}
while (!webSocket.CloseStatus.HasValue)
{
// Echo anything we receive
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, received.Count),
received.MessageType, received.EndOfMessage, CancellationToken.None);
await webSocket.CloseAsync(webSocket.CloseStatus.Value,
webSocket.CloseStatusDescription, CancellationToken.None);
}
}
This sample is configured using the same NowinServer as the previous one - the only difference is in how the
application is configured in its Configure method. A test using a simple websocket client demonstrates the
application:
OWIN environment
You can construct an OWIN environment using the HttpContext .
OWIN keys
OWIN depends on an IDictionary<string,object> object to communicate information throughout an HTTP
Request/Response exchange. ASP.NET Core implements the keys listed below. See the primary specification,
extensions, and OWIN Key Guidelines and Common Keys.
Request data (OWIN v1.0.0)
KEY VALUE (TYPE) DESCRIPTION
owin.RequestScheme String
owin.RequestMethod String
owin.RequestPathBase String
owin.RequestPath String
owin.RequestQueryString String
owin.RequestProtocol String
owin.RequestHeaders IDictionary<string,string[]>
KEY VALUE (TYPE) DESCRIPTION
owin.RequestBody Stream
owin.ResponseHeaders IDictionary<string,string[]>
owin.ResponseBody Stream
owin.CallCancelled CancellationToken
owin.Version String
Common keys
KEY VALUE (TYPE) DESCRIPTION
ssl.ClientCertificate X509Certificate
ssl.LoadClientCertAsync Func<Task>
server.RemoteIpAddress String
server.RemotePort String
server.LocalIpAddress String
server.LocalPort String
server.IsLocal bool
server.OnSendingHeaders Action<Action<object>,object>
SendFiles v0.3.0
KEY VALUE (TYPE) DESCRIPTION
Opaque v0.3.0
KEY VALUE (TYPE) DESCRIPTION
opaque.Version String
opaque.Stream Stream
opaque.CallCancelled CancellationToken
WebSocket v0.3.0
KEY VALUE (TYPE) DESCRIPTION
websocket.Version String
websocket.AcceptAlt Non-spec
websocket.CallCancelled CancellationToken
Additional resources
Middleware
Servers
WebSockets support in ASP.NET Core
7/3/2018 • 4 minutes to read • Edit Online
Prerequisites
ASP.NET Core 1.1 or later
Any OS that supports ASP.NET Core:
Windows 7 / Windows Server 2008 or later
Linux
macOS
If the app runs on Windows with IIS:
Windows 8 / Windows Server 2012 or later
IIS 8 / IIS 8 Express
WebSockets must be enabled in IIS (See the IIS/IIS Express support section.)
If the app runs on HTTP.sys:
Windows 8 / Windows Server 2012 or later
For supported browsers, see https://caniuse.com/#feat=websockets.
app.UseWebSockets();
});
A WebSocket request could come in on any URL, but this sample code only accepts requests for /ws .
Send and receive messages
The AcceptWebSocketAsync method upgrades the TCP connection to a WebSocket connection and provides a
WebSocket object. Use the WebSocket object to send and receive messages.
The code shown earlier that accepts the WebSocket request passes the WebSocket object to an Echo method. The
code receives a message and immediately sends back the same message. Messages are sent and received in a
loop until the client closes the connection:
private async Task Echo(HttpContext context, WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer),
CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType,
result.EndOfMessage, CancellationToken.None);
When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. Upon
closing the socket, the pipeline unwinds. That is, the request stops moving forward in the pipeline when the
WebSocket is accepted. When the loop is finished and the socket is closed, the request proceeds back up the
pipeline.
<system.webServer>
<webSocket enabled="false" />
</system.webServer>
Next steps
The sample app that accompanies this article is an echo app. It has a web page that makes WebSocket
connections, and the server resends any messages it receives back to the client. Run the app from a command
prompt (it's not set up to run from Visual Studio with IIS Express) and navigate to http://localhost:5000. The web
page shows the connection status in the upper left:
Select Connect to send a WebSocket request to the URL shown. Enter a test message and select Send. When
done, select Close Socket. The Communication Log section reports each open, send, and close action as it
happens.
Microsoft.AspNetCore.App metapackage for
ASP.NET Core 2.1
9/26/2018 • 3 minutes to read • Edit Online
This feature requires ASP.NET Core 2.1 and later targeting .NET Core 2.1 and later.
The Microsoft.AspNetCore.App metapackage for ASP.NET Core:
Does not include third-party dependencies except for Json.NET, Remotion.Linq, and IX-Async. These
3rd-party dependencies are deemed necessary to ensure the major frameworks features function.
Includes all supported packages by the ASP.NET Core team except those that contain third-party
dependencies (other than those previously mentioned).
Includes all supported packages by the Entity Framework Core team except those that contain third-
party dependencies (other than those previously mentioned).
All the features of ASP.NET Core 2.1 and later and Entity Framework Core 2.1 and later are included in the
Microsoft.AspNetCore.App package. The default project templates targeting ASP.NET Core 2.1 and later
use this package. We recommend applications targeting ASP.NET Core 2.1 and later and Entity Framework
Core 2.1 and later use the Microsoft.AspNetCore.App package.
The version number of the Microsoft.AspNetCore.App metapackage represents the ASP.NET Core version
and Entity Framework Core version.
Using the Microsoft.AspNetCore.App metapackage provides version restrictions that protect your app:
If a package is included that has a transitive (not direct) dependency on a package in
Microsoft.AspNetCore.App , and those version numbers differ, NuGet will generate an error.
Other packages added to your app cannot change the version of packages included in
Microsoft.AspNetCore.App .
Version consistency ensures a reliable experience. Microsoft.AspNetCore.App was designed to prevent
untested version combinations of related bits being used together in the same app.
Applications that use the Microsoft.AspNetCore.App metapackage automatically take advantage of the
ASP.NET Core shared framework. When you use the Microsoft.AspNetCore.App metapackage, no assets
from the referenced ASP.NET Core NuGet packages are deployed with the application—the ASP.NET
Core shared framework contains these assets. The assets in the shared framework are precompiled to
improve application startup time. For more information, see "shared framework" in .NET Core distribution
packaging.
The following project file references the Microsoft.AspNetCore.App metapackage for ASP.NET Core and
represents a typical ASP.NET Core 2.1 template:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
The preceding markup represents a typical ASP.NET Core 2.1 and later template. It doesn't specify a
version number for the Microsoft.AspNetCore.App package reference. When the version is not specified, an
implicit version is specified by the SDK, that is, Microsoft.NET.Sdk.Web . We recommend relying on the
implicit version specified by the SDK and not explicitly setting the version number on the package
reference. If you have questions on this approach, leave a GitHub comment at the Discussion for the
Microsoft.AspNetCore.App implicit version.
The implicit version is set to major.minor.0 for portable apps. The shared framework roll-forward
mechanism will run the app on the latest compatible version among the installed shared frameworks. To
guarantee the same version is used in development, test, and production, ensure the same version of the
shared framework is installed in all environments. For self contained apps, the implicit version number is
set to the major.minor.patch of the shared framework bundled in the installed SDK.
Specifying a version number on the Microsoft.AspNetCore.App reference does not guarantee that version
of the shared framework will be chosen. For example, suppose version "2.1.1" is specified, but "2.1.3" is
installed. In that case, the app will use "2.1.3". Although not recommended, you can disable roll forward
(patch and/or minor). For more information regarding dotnet host roll-forward and how to configure its
behavior, see dotnet host roll forward.
<Project Sdk must be set to Microsoft.NET.Sdk.Web to use the implicit version Microsoft.AspNetCore.App .
When <Project Sdk="Microsoft.NET.Sdk"> (without the trailing .Web ) is used:
The following warning is generated:
Warning NU1604: Project dependency Microsoft.AspNetCore.App does not contain an inclusive
lower bound. Include a lower bound in the dependency version to ensure consistent restore results.
This is a known issue with the .NET Core 2.1 SDK and will be fixed in the .NET Core 2.2 SDK.
NOTE
We recommend applications targeting ASP.NET Core 2.1 and later use the Microsoft.AspNetCore.App rather than this
package. See Migrating from Microsoft.AspNetCore.All to Microsoft.AspNetCore.App in this article.
This feature requires ASP.NET Core 2.x targeting .NET Core 2.x.
The Microsoft.AspNetCore.All metapackage for ASP.NET Core includes:
All supported packages by the ASP.NET Core team.
All supported packages by the Entity Framework Core.
Internal and 3rd-party dependencies used by ASP.NET Core and Entity Framework Core.
All the features of ASP.NET Core 2.x and Entity Framework Core 2.x are included in the
Microsoft.AspNetCore.All package. The default project templates targeting ASP.NET Core 2.0 use this
package.
The version number of the Microsoft.AspNetCore.All metapackage represents the ASP.NET Core version and
Entity Framework Core version.
Applications that use the Microsoft.AspNetCore.All metapackage automatically take advantage of the .NET
Core Runtime Store. The Runtime Store contains all the runtime assets needed to run ASP.NET Core 2.x
applications. When you use the Microsoft.AspNetCore.All metapackage, no assets from the referenced
ASP.NET Core NuGet packages are deployed with the application — the .NET Core Runtime Store contains
these assets. The assets in the Runtime Store are precompiled to improve application startup time.
You can use the package trimming process to remove packages that you don't use. Trimmed packages are
excluded in published application output.
The following .csproj file references the Microsoft.AspNetCore.All metapackage for ASP.NET Core:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
</Project>
To move from Microsoft.AspNetCore.All to Microsoft.AspNetCore.App , if your app uses any APIs from the
above packages, or packages brought in by those packages, add references to those packages in your project.
Any dependencies of the preceding packages that otherwise aren't dependencies of Microsoft.AspNetCore.App
are not included implicitly. For example:
StackExchange.Redis as a dependency of Microsoft.Extensions.Caching.Redis
Microsoft.ApplicationInsights as a dependency of
Microsoft.AspNetCore.ApplicationInsights.HostingStartup
Choose between ASP.NET and ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
No matter the web app you're creating, ASP.NET has a solution for you: from enterprise web apps targeting
Windows Server, to small microservices targeting Linux containers, and everything in between.
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
ASP.NET is a mature framework that provides all the services needed to build enterprise-grade, server-based web
apps on Windows.
Framework selection
Review the table below to determine which framework is most appropriate for your needs.
Razor Pages is the recommended approach to create a Web Use Web Forms, SignalR, MVC, Web API, WebHooks, or Web
UI as of ASP.NET Core 2.x. See also MVC, Web API, and Pages
SignalR.
Develop with Visual Studio, Visual Studio for Mac, or Visual Develop with Visual Studio using C#, VB, or F#
Studio Code using C# or F#
Choose .NET Framework or .NET Core runtime Use .NET Framework runtime
ASP.NET scenarios
Websites
APIs
Real-time
Resources
Introduction to ASP.NET
Introduction to ASP.NET Core
Introduction to Razor Pages in ASP.NET Core
9/21/2018 • 19 minutes to read • Edit Online
Prerequisites
Install one of the following:
CLI tooling: Windows, Linux, or macOS: .NET Core SDK 2.0 or later
IDE/editor tooling
Windows: Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
Linux: Visual Studio Code
macOS: Visual Studio for Mac
Razor Pages
Razor Pages is enabled in Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}
@page
<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>
The preceding code looks a lot like a Razor view file. What makes it different is the @page directive. @page
makes the file into an MVC action - which means that it handles requests directly, without going through a
controller. @page must be the first Razor directive on a page. @page affects the behavior of other Razor
constructs.
A similar page, using a PageModel class, is shown in the following two files. The Pages/Index2.cshtml file:
@page
@using RazorPagesIntro.Pages
@model IndexModel2
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace RazorPagesIntro.Pages
{
public class IndexModel2 : PageModel
{
public string Message { get; private set; } = "PageModel in C#";
By convention, the PageModel class file has the same name as the Razor Page file with .cs appended. For
example, the previous Razor Page is Pages/Index2.cshtml. The file containing the PageModel class is named
Pages/Index2.cshtml.cs.
The associations of URL paths to pages are determined by the page's location in the file system. The
following table shows a Razor Page path and the matching URL:
/Pages/Index.cshtml / or /Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
Notes:
The runtime looks for Razor Pages files in the Pages folder by default.
Index is the default page when a URL doesn't include a page.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;
namespace RazorPagesContacts
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }
namespace RazorPagesContacts.Data
{
public class Customer
{
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
}
}
The db context:
using Microsoft.EntityFrameworkCore;
namespace RazorPagesContacts.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>
namespace RazorPagesContacts.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}
By convention, the PageModel class is called <PageName>Model and is in the same namespace as the page.
The PageModel class allows separation of the logic of a page from its presentation. It defines page handlers
for requests sent to the page and the data used to render the page. This separation allows you to manage
page dependencies through dependency injection and to unit test the pages.
The page has an OnPostAsync handler method, which runs on POST requests (when a user posts the form).
You can add handler methods for any HTTP verb. The most common handlers are:
OnGet to initialize state needed for the page. OnGet sample.
OnPost to handle form submissions.
The Asyncnaming suffix is optional but is often used by convention for asynchronous functions. The
OnPostAsync code in the preceding example looks similar to what you would normally write in a controller.
The preceding code is typical for Razor Pages. Most of the MVC primitives like model binding, validation, and
action results are shared.
The previous OnPostAsync method:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
Razor Pages, by default, bind properties only with non-GET verbs. Binding to properties can reduce the
amount of code you have to write. Binding reduces code by using the same property to render form fields (
<input asp-for="Customer.Name" /> ) and accept the input.
NOTE
For security reasons, you must opt in to binding GET request data to page model properties. Verify user input before
mapping it to properties. Opting in to this behavior is useful when addressing scenarios which rely on query string or
route values.
To bind a property on GET requests, set the [BindProperty] attribute's SupportsGet property to true :
[BindProperty(SupportsGet = true)]
@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h1>Contacts</h1>
<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model.Customers)
{
<tr>
<td>@contact.Id</td>
<td>@contact.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
</td>
</tr>
}
</tbody>
</table>
<a asp-page="./Create">Create</a>
</form>
namespace RazorPagesContacts.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
}
}
The Index.cshtml file contains the following markup to create an edit link for each contact:
The Anchor Tag Helper used the asp-route-{value} attribute to generate a link to the Edit page. The link
contains route data with the contact ID. For example, http://localhost:5000/Edit/1 .
The Pages/Edit.cshtml file:
@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Edit Customer";
}
<div>
<button type="submit">Save</button>
</div>
</form>
The first line contains the @page "{id:int}" directive. The routing constraint "{id:int}" tells the page to
accept requests to the page that contain int route data. If a request to the page doesn't contain route data
that can be converted to an int , the runtime returns an HTTP 404 (not found) error. To make the ID
optional, append ? to the route constraint:
@page "{id:int?}"
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
[BindProperty]
public Customer Customer { get; set; }
if (Customer == null)
{
return RedirectToPage("/Index");
}
return Page();
}
_db.Attach(Customer).State = EntityState.Modified;
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}
return RedirectToPage("/Index");
}
}
}
The Index.cshtml file also contains markup to create a delete button for each customer contact:
When the delete button is rendered in HTML, its formaction includes parameters for:
The customer contact ID specified by the asp-route-id attribute.
The handler specified by the asp-page-handler attribute.
When the button is selected, a form POST request is sent to the server. By convention, the name of the
handler method is selected based the value of the handler parameter according to the scheme
OnPost[handler]Async .
Because the handler is delete in this example, the OnPostDeleteAsync handler method is used to process
the POST request. If the asp-page-handler is set to a different value, such as remove , a page handler method
with the name OnPostRemoveAsync is selected.
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
[Required(ErrorMessage = "Color is required")]
public string Color { get; set; }
// Process color.
return RedirectToPage("./Index");
}
}
}
If no HEAD handler ( OnHead ) is defined, Razor Pages falls back to calling the GET page handler ( OnGet ) in
ASP.NET Core 2.1 or later. Opt in to this behavior with the SetCompatibilityVersion method in
Startup.Configure for ASP.NET Core 2.1 to 2.x:
services.AddMvc()
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1);
<!DOCTYPE html>
<html>
<head>
<title>Razor Pages Sample</title>
</head>
<body>
<a asp-page="/Index">Home</a>
@RenderBody()
<a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>
The Layout:
Controls the layout of each page (unless the page opts out of layout).
Imports HTML structures such as JavaScript and stylesheets.
See layout page for more information.
The Layout property is set in Pages/_ViewStart.cshtml:
@{
Layout = "_Layout";
}
The layout is in the Pages/Shared folder. Pages look for other views (layouts, templates, partials)
hierarchically, starting in the same folder as the current page. A layout in the Pages/Shared folder can be
used from any Razor page under the Pages folder.
The layout file should go in the Pages/Shared folder.
The layout is in the Pages folder. Pages look for other views (layouts, templates, partials) hierarchically,
starting in the same folder as the current page. A layout in the Pages folder can be used from any Razor page
under the Pages folder.
We recommend you not put the layout file in the Views/Shared folder. Views/Shared is an MVC views
pattern. Razor Pages are meant to rely on folder hierarchy, not path conventions.
View search from a Razor Page includes the Pages folder. The layouts, templates, and partials you're using
with MVC controllers and conventional Razor views just work.
Add a Pages/_ViewImports.cshtml file:
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@namespace is explained later in the tutorial. The @addTagHelper directive brings in the built-in Tag Helpers to
all the pages in the Pages folder.
When the @namespace directive is used explicitly on a page:
@page
@namespace RazorPagesIntro.Pages.Customers
@model NameSpaceModel
<h2>Name space</h2>
<p>
@Model.Message
</p>
The directive sets the namespace for the page. The @model directive doesn't need to include the namespace.
When the @namespace directive is contained in _ViewImports.cshtml, the specified namespace supplies the
prefix for the generated namespace in the Page that imports the @namespace directive. The rest of the
generated namespace (the suffix portion) is the dot-separated relative path between the folder containing
_ViewImports.cshtml and the folder containing the page.
For example, the PageModel class Pages/Customers/Edit.cshtml.cs explicitly sets the namespace:
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The generated namespace for the Pages/Customers/Edit.cshtml Razor Page is the same as the PageModel
class.
@namespace also works with conventional Razor views.
The original Pages/Create.cshtml view file:
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>
@page
@model CreateModel
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>
The Razor Pages starter project contains the Pages/_ValidationScriptsPartial.cshtml, which hooks up client-
side validation.
For more information on partial views, see Partial views in ASP.NET Core.
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The page name is the path to the page from the root /Pages folder including a leading / (for example,
/Index ). The preceding URL generation samples offer enhanced options and functional capabilities over
hardcoding a URL. URL generation uses routing and can generate and encode parameters according to how
the route is defined in the destination path.
URL generation for pages supports relative names. The following table shows which Index page is selected
with different RedirectToPage parameters from Pages/Customers/Create.cshtml:
REDIRECTTOPAGE(X) PAGE
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index
ViewData attribute
Data can be passed to a page with ViewDataAttribute. Properties on controllers or Razor Page models
decorated with [ViewData] have their values stored and loaded from the ViewDataDictionary.
In the following example, the AboutModel contains a Title property decorated with [ViewData] . The Title
property is set to the title of the About page:
public class AboutModel : PageModel
{
[ViewData]
public string Title { get; } = "About";
<h1>@Model.Title</h1>
<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"] - WebApplication</title>
...
TempData
ASP.NET Core exposes the TempData property on a controller. This property stores data until it's read. The
Keep and Peek methods can be used to examine the data without deletion. TempData is useful for
redirection, when data is needed for more than a single request.
The [TempData] attribute is new in ASP.NET Core 2.0 and is supported on controllers and pages.
The following code sets the value of Message using TempData :
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;
[TempData]
public string Message { get; set; }
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}
The following markup in the Pages/Customers/Index.cshtml file displays the value of Message using
TempData .
<h3>Msg: @Model.Message</h3>
The Pages/Customers/Index.cshtml.cs page model applies the [TempData] attribute to the Message property.
[TempData]
public string Message { get; set; }
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>
The form in the preceding example has two submit buttons, each using the FormActionTagHelper to submit to
a different URL. The asp-page-handler attribute is a companion to asp-page . asp-page-handler generates
URLs that submit to each of the handler methods defined by a page. asp-page isn't specified because the
sample is linking to the current page.
The page model:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages.Customers
{
public class CreateFATHModel : PageModel
{
private readonly AppDbContext _db;
[BindProperty]
public Customer Customer { get; set; }
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The preceding code uses named handler methods. Named handler methods are created by taking the text in
the name after On<HTTP Verb> and before Async (if present). In the preceding example, the page methods
are OnPostJoinListAsync and OnPostJoinListUCAsync. With OnPost and Async removed, the handler
names are JoinList and JoinListUC .
Custom routes
Use the @page directive to:
Specify a custom route to a page. For example, the route to the About page can be set to
/Some/Other/Path with @page "/Some/Other/Path" .
Append segments to a page's default route. For example, an "item" segment can be added to a page's
default route with @page "item" .
Append parameters to a page's default route. For example, an ID parameter, id , can be required for a
page with @page "{id}" .
A root-relative path designated by a tilde ( ~ ) at the beginning of the path is supported. For example,
@page "~/Some/Other/Path" is the same as @page "/Some/Other/Path" .
You can change the query string ?handler=JoinList in the URL to a route segment /JoinList by specifying
the route template @page "{handler?}" .
If you don't like the query string ?handler=JoinList in the URL, you can change the route to put the handler
name in the path portion of the URL. You can customize the route by adding a route template enclosed in
double quotes after the @page directive.
@page "{handler?}"
@model CreateRouteModel
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>
Using the preceding code, the URL path that submits to OnPostJoinListAsync is
http://localhost:5000/Customers/CreateFATH/JoinList . The URL path that submits to OnPostJoinListUCAsync
is http://localhost:5000/Customers/CreateFATH/JoinListUC .
The ? following handler means the route parameter is optional.
Currently you can use the RazorPagesOptions to set the root directory for pages, or add application model
conventions for pages. We'll enable more extensibility this way in the future.
To precompile views, see Razor view compilation .
Download or view sample code.
See Get started with Razor Pages, which builds on this introduction.
Specify that Razor Pages are at the content root
By default, Razor Pages are rooted in the /Pages directory. Add WithRazorPagesAtContentRoot to AddMvc
to specify that your Razor Pages are at the content root (ContentRootPath) of the app:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesAtContentRoot();
services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesRoot("/path/to/razor/pages");
Additional resources
Introduction to ASP.NET Core
Razor syntax reference for ASP.NET Core
Get started with Razor Pages in ASP.NET Core
Razor Pages authorization conventions in ASP.NET Core
Razor Pages route and app conventions in ASP.NET Core
Razor Pages unit tests in ASP.NET Core
Partial views in ASP.NET Core
Filter methods for Razor Pages in ASP.NET Core
9/18/2018 • 4 minutes to read • Edit Online
By Rick Anderson
Razor Page filters IPageFilter and IAsyncPageFilter allow Razor Pages to run code before and after a Razor Page
handler is run. Razor Page filters are similar to ASP.NET Core MVC action filters, except they can't be applied to
individual page handler methods.
Razor Page filters:
Run code after a handler method has been selected, but before model binding occurs.
Run code before the handler method executes, after model binding is complete.
Run code after the handler method executes.
Can be implemented on a page or globally.
Cannot be applied to specific page handler methods.
Code can be run before a handler method executes using the page constructor or middleware, but only Razor Page
filters have access to HttpContext. Filters have a FilterContext derived parameter, which provides access to
HttpContext . For example, the Implement a filter attribute sample adds a header to the response, something that
can't be done with constructors or middleware.
View or download sample code (how to download)
Razor Page filters provide the following methods, which can be applied globally or at the page level:
Synchronous methods:
OnPageHandlerSelected : Called after a handler method has been selected, but before model binding
occurs.
OnPageHandlerExecuting : Called before the handler method executes, after model binding is complete.
OnPageHandlerExecuted : Called after the handler method executes, before the action result.
Asynchronous methods:
OnPageHandlerSelectionAsync : Called asynchronously after the handler method has been selected, but
before model binding occurs.
OnPageHandlerExecutionAsync : Called asynchronously before the handler method is invoked, after
model binding is complete.
NOTE
Implement either the synchronous or the async version of a filter interface, not both. The framework checks first to see if the
filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's method(s). If both
interfaces are implemented, only the async methods are be called. The same rule applies to overrides in pages, implement the
synchronous or the async version of the override, not both.
namespace PageFilter.Filters
{
public class SampleAsyncPageFilter : IAsyncPageFilter
{
private readonly ILogger _logger;
In the preceding code, ILogger is not required. It's used in the sample to provide trace information for the
application.
The following code enables the SampleAsyncPageFilter in the Startup class:
namespace PageFilter
{
public class Startup
{
ILogger _logger;
public Startup(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_logger = loggerFactory.CreateLogger<GlobalFiltersLogger>();
Configuration = configuration;
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
}
}
The following code calls AddFolderApplicationModelConvention to apply the SampleAsyncPageFilter to only pages in
/subFolder:
namespace PageFilter.Filters
{
public class SamplePageFilter : IPageFilter
{
private readonly ILogger _logger;
namespace PageFilter.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger _logger;
namespace PageFilter.Filters
{
public class AddHeaderAttribute : ResultFilterAttribute
{
private readonly string _name;
private readonly string _value;
[AddHeader("Author", "Rick")]
public class ContactModel : PageModel
{
private readonly ILogger _logger;
See Overriding the default order for instructions on overriding the order.
See Cancellation and short circuiting for instructions to short-circuit the filter pipeline from a filter.
namespace PageFilter.Pages
{
[Authorize]
public class ModelWithAuthFilterModel : PageModel
{
public IActionResult OnGet() => Page();
}
}
Create reusable UI using the Razor Class Library
project in ASP.NET Core
9/21/2018 • 5 minutes to read • Edit Online
By Rick Anderson
Razor views, pages, controllers, page models, View components, and data models can be built into a Razor Class
Library (RCL ). The RCL can be packaged and reused. Applications can include the RCL and override the views and
pages it contains. When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor
markup (.cshtml file) in the web app takes precedence.
This feature requires .NET Core 2.1 SDK or later
View or download sample code (how to download)
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" />
</ItemGroup>
</Project>
<p>RazorUIClassLib\Areas\MyFeature\Pages\Shared\_Message.cshtml</p>
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<body>
<partial name="_Header">
@RenderBody()
<partial name="_Footer">
</body>
Razor Pages route and app conventions in ASP.NET
Core
9/18/2018 • 17 minutes to read • Edit Online
By Luke Latham
Learn how to use page route and app model provider conventions to control page routing, discovery, and
processing in Razor Pages apps.
When you need to configure custom page routes for individual pages, configure routing to pages with the
AddPageRoute convention described later in this topic.
To specify a page route, add route segments, or add parameters to a route, use the page's @page directive. For
more information, see Custom routes.
There are reserved words that can't be used as route segments or parameter names. For more information, see
Routing: Reserved routing names.
View or download sample code (how to download)
Conventions.Add
IPageRouteModelConvention
IPageApplicationModelConvention
Page route action conventions Add a route template to pages in a folder and to a single
AddFolderRouteModelConvention page.
AddPageRouteModelConvention
AddPageRoute
Page model action conventions Add a header to pages in a folder, add a header to a single
AddFolderApplicationModelConvention page, and configure a filter factory to add a header to an
AddPageApplicationModelConvention app's pages.
ConfigureFilter (filter class, lambda expression, or filter
factory)
Default page app model provider Replace the default page model provider to change the
conventions for handler names.
Conventions.Add
IPageRouteModelConvention
IPageApplicationModelConvention
IPageHandlerModelConvention
SCENARIO THE SAMPLE DEMONSTRATES ...
Page route action conventions Add a route template to pages in a folder and to a single
AddFolderRouteModelConvention page.
AddPageRouteModelConvention
AddPageRoute
Page model action conventions Add a header to pages in a folder, add a header to a single
AddFolderApplicationModelConvention page, and configure a filter factory to add a header to an
AddPageApplicationModelConvention app's pages.
ConfigureFilter (filter class, lambda expression, or filter
factory)
Default page app model provider Replace the default page model provider to change the
conventions for handler names.
Razor Pages conventions are added and configured using the AddRazorPagesOptions extension method to
AddMvc on the service collection in the Startup class. The following convention examples are explained later in
this topic:
Route order
Routes specify an Order for processing (route matching).
ORDER BEHAVIOR
Model conventions
Add a delegate for IPageConvention to add model conventions that apply to Razor Pages.
Add a route model convention to all pages
Use Conventions to create and add an IPageRouteModelConvention to the collection of IPageConvention
instances that are applied during page route model construction.
The sample app adds a {globalTemplate?} route template to all of the pages in the app:
The Order property for the AttributeRouteModel is set to 1 . This ensures the following route matching behavior
in the sample app:
A route template for TheContactPage/{text?} is added later in the topic. The Contact Page route has a default
order of null ( Order = 0 ), so it matches before the {globalTemplate?} route template.
An {aboutTemplate?} route template is added later in the topic. The {aboutTemplate?} template is given an
Order of 2 . When the About page is requested at /About/RouteDataValue , "RouteDataValue" is loaded into
RouteData.Values["globalTemplate"] ( Order = 1 ) and not RouteData.Values["aboutTemplate"] ( Order = 2 ) due
to setting the Order property.
An {otherPagesTemplate?} route template is added later in the topic. The {otherPagesTemplate?} template is
given an Order of 2 . When any page in the Pages/OtherPages folder is requested with a route parameter (for
example, /OtherPages/Page1/RouteDataValue ), "RouteDataValue" is loaded into
RouteData.Values["globalTemplate"] ( Order = 1 ) and not RouteData.Values["otherPagesTemplate"] ( Order = 2 )
due to setting the Order property.
Wherever possible, don't set the Order , which results in Order = 0 . Rely on routing to select the correct route.
Razor Pages options, such as adding Conventions, are added when MVC is added to the service collection in
Startup.ConfigureServices . For an example, see the sample app.
options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());
Request the sample's About page at localhost:5000/About/GlobalRouteValue and inspect the result:
Startup.cs:
options.Conventions.Add(new GlobalHeaderPageApplicationModelConvention());
Request the sample's About page at localhost:5000/About and inspect the headers to view the result:
Add a handler model convention to all pages
Use Conventions to create and add an IPageHandlerModelConvention to the collection of IPageConvention
instances that are applied during page handler model construction.
Startup.ConfigureServices :
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.Add(new GlobalPageHandlerModelConvention());
});
The Order property for the AttributeRouteModel is set to 2 . This ensures that the template for
{globalTemplate?} (set earlier in the topic to 1 ) is given priority for the first route data value position when a
single route value is provided. If a page in the Pages/OtherPages folder is requested with a route parameter value
(for example, /OtherPages/Page1/RouteDataValue ), "RouteDataValue" is loaded into
RouteData.Values["globalTemplate"] ( Order = 1 ) and not RouteData.Values["otherPagesTemplate"] ( Order = 2 )
due to setting the Order property.
Wherever possible, don't set the Order , which results in Order = 0 . Rely on routing to select the correct route.
Request the sample's Page1 page at localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue and
inspect the result:
The Order property for the AttributeRouteModel is set to 2 . This ensures that the template for
{globalTemplate?} (set earlier in the topic to 1 ) is given priority for the first route data value position when a
single route value is provided. If the About page is requested with a route parameter value at
/About/RouteDataValue , "RouteDataValue" is loaded into RouteData.Values["globalTemplate"] ( Order = 1 ) and not
RouteData.Values["aboutTemplate"] ( Order = 2 ) due to setting the Order property.
Wherever possible, don't set the Order , which results in Order = 0 . Rely on routing to select the correct route.
Request the sample's About page at localhost:5000/About/GlobalRouteValue/AboutRouteValue and inspect the result:
options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");
The Contact page can also be reached at /Contact via its default route.
The sample app's custom route to the Contact page allows for an optional text route segment ( {text?} ). The
page also includes this optional segment in its @page directive in case the visitor accesses the page at its /Contact
route:
@page "{text?}"
@model ContactModel
@{
ViewData["Title"] = "Contact";
}
<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>
<address>
One Microsoft Way<br>
Redmond, WA 98052-6399<br>
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong> <a href="mailto:[email protected]">[email protected]</a><br>
<strong>Marketing:</strong> <a href="mailto:[email protected]">[email protected]</a>
</address>
<p>@Model.RouteDataTextTemplateValue</p>
Note that the URL generated for the Contact link in the rendered page reflects the updated route:
Visit the Contact page at either its ordinary route, /Contact , or the custom route, /TheContactPage . If you supply
an additional text route segment, the page shows the HTML -encoded segment that you provide:
Page model action conventions
The default page model provider that implements IPageApplicationModelProvider invokes conventions which are
designed to provide extensibility points for configuring page models. These conventions are useful when building
and modifying page discovery and processing scenarios.
For the examples in this section, the sample app uses an AddHeaderAttribute class, which is a ResultFilterAttribute,
that applies a response header:
Using conventions, the sample demonstrates how to apply the attribute to all of the pages in a folder and to a
single page.
Folder app model convention
Use AddFolderApplicationModelConvention to create and add an IPageApplicationModelConvention that invokes
an action on PageApplicationModel instances for all pages under the specified folder.
The sample demonstrates the use of AddFolderApplicationModelConvention by adding a header, OtherPagesHeader ,
to the pages inside the OtherPages folder of the app:
Request the sample's Page1 page at localhost:5000/OtherPages/Page1 and inspect the headers to view the result:
Request the sample's About page at localhost:5000/About and inspect the headers to view the result:
Configure a filter
ConfigureFilter configures the specified filter to apply. You can implement a filter class, but the sample app shows
how to implement a filter in a lambda expression, which is implemented behind-the-scenes as a factory that
returns a filter:
options.Conventions.ConfigureFilter(model =>
{
if (model.RelativePath.Contains("OtherPages/Page2"))
{
return new AddHeaderAttribute(
"OtherPagesPage2Header",
new string[] { "OtherPages/Page2 Header Value" });
}
return new EmptyFilter();
});
The page app model is used to check the relative path for segments that lead to the Page2 page in the OtherPages
folder. If the condition passes, a header is added. If not, the EmptyFilter is applied.
EmptyFilter is an Action filter. Since Action filters are ignored by Razor Pages, the EmptyFilter no-ops as
intended if the path doesn't contain OtherPages/Page2 .
Request the sample's Page2 page at localhost:5000/OtherPages/Page2 and inspect the headers to view the result:
options.Conventions.ConfigureFilter(new AddHeaderWithFactory());
AddHeaderWithFactory.cs:
public class AddHeaderWithFactory : IFilterFactory
{
// Implement IFilterFactory
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new AddHeaderFilter();
}
Request the sample's About page at localhost:5000/About and inspect the headers to view the result:
if (!IsHandler(method))
{
return null;
}
if (!TryParseHandlerMethod(
method.Name, out var httpMethod, out var handlerName))
{
return null;
}
handlerModel.Parameters.Add(parameterModel);
handlerModel.Parameters.Add(parameterModel);
}
return handlerModel;
}
if (length == 0)
{
// The method is named "Async". Exit processing.
return false;
}
services.AddSingleton<IPageApplicationModelProvider,
CustomPageApplicationModelProvider>();
The page model in Index.cshtml.cs shows how the ordinary handler method naming conventions are changed for
pages in the app. The ordinary "On" prefix naming used with Razor Pages is removed. The method that initializes
the page state is now named Get . You can see this convention used throughout the app if you open any page
model for any of the pages.
Each of the other methods start with the HTTP verb that describes its processing. The two methods that start with
Delete would normally be treated as DELETE HTTP verbs, but the logic in TryParseHandlerMethod explicitly sets
the verb to POST for both handlers.
Note that Async is optional between DeleteAllMessages and DeleteMessageAsync . They're both asynchronous
methods, but you can choose to use the Async postfix or not; we recommend that you do. DeleteAllMessages is
used here for demonstration purposes, but we recommend that you name such a method DeleteAllMessagesAsync .
It doesn't affect the processing of the sample's implementation, but using the Async postfix calls out the fact that
it's an asynchronous method.
public async Task Get()
{
Messages = await _db.Messages.AsNoTracking().ToListAsync();
}
return RedirectToPage();
}
return RedirectToPage();
}
if (message != null)
{
_db.Messages.Remove(message);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
Note the handler names provided in Index.cshtml match the DeleteAllMessages and DeleteMessageAsync handler
methods:
<div class="row">
<div class="col-md-3">
<form method="post">
<h2>Clear all messages</h2>
<hr>
<div class="form-group">
<button type="submit" asp-page-handler="DeleteAllMessages"
class="btn btn-danger">Clear All</button>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form method="post">
<h2>Messages</h2>
<hr>
<ol>
@foreach (var message in Model.Messages)
{
<li>
@message.Text
<button type="submit" asp-page-handler="DeleteMessage"
class="btn btn-danger"
asp-route-id="@message.Id">Delete</button>
</li>
}
</ol>
</form>
</div>
Async in the handler method name DeleteMessageAsync is factored out by the TryParseHandlerMethod for handler
matching of POST request to method. The asp-page-handler name of DeleteMessage is matched to the handler
method DeleteMessageAsync .
Additional resources
Razor Pages authorization conventions
Upload files to a Razor Page in ASP.NET Core
7/11/2018 • 18 minutes to read • Edit Online
By Luke Latham
This topic builds upon the sample app in Get started with Razor Pages in ASP.NET Core.
This topic shows how to use simple model binding to upload files, which works well for uploading small files. For
information on streaming large files, see Uploading large files with streaming.
In the following steps, a movie schedule file upload feature is added to the sample app. A movie schedule is
represented by a Schedule class. The class includes two versions of the schedule. One version is provided to
customers, PublicSchedule . The other version is used for company employees, PrivateSchedule . Each version is
uploaded as a separate file. The tutorial demonstrates how to perform two file uploads from a page with a single
POST to the server.
Security considerations
Caution must be taken when providing users with the ability to upload files to a server. Attackers may execute
denial of service and other attacks on a system. Some security steps that reduce the likelihood of a successful
attack are:
Upload files to a dedicated file upload area on the system, which makes it easier to impose security measures
on uploaded content. When permitting file uploads, make sure that execute permissions are disabled on the
upload location.
Use a safe file name determined by the app, not from user input or the file name of the uploaded file.
Only allow a specific set of approved file extensions.
Verify client-side checks are performed on the server. Client-side checks are easy to circumvent.
Check the size of the upload and prevent larger uploads than expected.
Run a virus/malware scanner on uploaded content.
WARNING
Uploading malicious code to a system is frequently the first step to executing code that can:
Completely takeover a system.
Overload a system with the result that the system completely fails.
Compromise user or system data.
Apply graffiti to a public interface.
namespace RazorPagesMovie.Models
{
public class FileUpload
{
[Required]
[Display(Name="Title")]
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }
[Required]
[Display(Name="Public Schedule")]
public IFormFile UploadPublicSchedule { get; set; }
[Required]
[Display(Name="Private Schedule")]
public IFormFile UploadPrivateSchedule { get; set; }
}
}
using Microsoft.AspNetCore.Http;
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.Models
{
public class FileUpload
{
[Required]
[Display(Name="Title")]
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }
[Required]
[Display(Name="Public Schedule")]
public IFormFile UploadPublicSchedule { get; set; }
[Required]
[Display(Name="Private Schedule")]
public IFormFile UploadPrivateSchedule { get; set; }
}
}
The class has a property for the schedule's title and a property for each of the two versions of the schedule. All
three properties are required, and the title must be 3-60 characters long.
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Utilities
{
public class FileHelpers
{
public static async Task<string> ProcessFormFile(IFormFile formFile,
ModelStateDictionary modelState)
{
var fieldDisplayName = string.Empty;
if (property != null)
{
var displayAttribute =
property.GetCustomAttribute(typeof(DisplayAttribute))
as DisplayAttribute;
if (displayAttribute != null)
{
fieldDisplayName = $"{displayAttribute.Name} ";
}
}
if (formFile.ContentType.ToLower() != "text/plain")
{
modelState.AddModelError(formFile.Name,
$"The {fieldDisplayName}file ({fileName}) must be a text file.");
}
return string.Empty;
}
}
}
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Utilities
{
public class FileHelpers
{
public static async Task<string> ProcessFormFile(
IFormFile formFile, ModelStateDictionary modelState)
{
var fieldDisplayName = string.Empty;
if (property != null)
{
var displayAttribute =
property.GetCustomAttribute(typeof(DisplayAttribute))
as DisplayAttribute;
if (displayAttribute != null)
{
fieldDisplayName = $"{displayAttribute.Name} ";
}
}
if (formFile.ContentType.ToLower() != "text/plain")
{
modelState.AddModelError(formFile.Name,
$"The {fieldDisplayName}file ({fileName}) must be a text file.");
}
return string.Empty;
}
}
}
return RedirectToPage("./Index");
}
The worker process must have write permissions to the location specified by filePath .
NOTE
The filePath must include the file name. If the file name isn't provided, an UnauthorizedAccessException is thrown at
runtime.
WARNING
Never persist uploaded files in the same directory tree as the app.
The code sample provides no server-side protection against malicious file uploads. For information on reducing the attack
surface area when accepting files from users, see the following resources:
Unrestricted File Upload
Azure Security: Ensure appropriate controls are in place when accepting files from users
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models
{
public class Schedule
{
public int ID { get; set; }
public string Title { get; set; }
namespace RazorPagesMovie.Models
{
public class Schedule
{
public int ID { get; set; }
public string Title { get; set; }
The class uses Display and DisplayFormat attributes, which produce friendly titles and formatting when the
schedule data is rendered.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace RazorPagesMovie.Models
{
public class RazorPagesMovieContext : DbContext
{
public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
: base(options)
{
}
namespace RazorPagesMovie.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{
}
In the PMC, execute the following commands. These commands add a Schedule table to the database:
Add-Migration AddScheduleTable
Update-Database
@page
@model RazorPagesMovie.Pages.Schedules.IndexModel
@{
ViewData["Title"] = "Schedules";
}
<h2>Schedules</h2>
<hr />
<h3>Upload Schedules</h3>
<div class="row">
<div class="col-md-4">
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="FileUpload.Title" class="control-label"></label>
<input asp-for="FileUpload.Title" type="text" class="form-control" />
<span asp-validation-for="FileUpload.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FileUpload.UploadPublicSchedule" class="control-label"></label>
<input asp-for="FileUpload.UploadPublicSchedule" type="file" class="form-control"
style="height:auto" />
<span asp-validation-for="FileUpload.UploadPublicSchedule" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FileUpload.UploadPrivateSchedule" class="control-label"></label>
<input asp-for="FileUpload.UploadPrivateSchedule" type="file" class="form-control"
style="height:auto" />
<span asp-validation-for="FileUpload.UploadPrivateSchedule" class="text-danger"></span>
</div>
<input type="submit" value="Upload" class="btn btn-default" />
</form>
</div>
</div>
<h3>Loaded Schedules</h3>
<table class="table">
<thead>
<tr>
<th></th>
<th>
@Html.DisplayNameFor(model => model.Schedule[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Schedule[0].UploadDT)
</th>
<th class="text-center">
@Html.DisplayNameFor(model => model.Schedule[0].PublicScheduleSize)
</th>
<th class="text-center">
@Html.DisplayNameFor(model => model.Schedule[0].PrivateScheduleSize)
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Schedule) {
<tr>
<td>
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.UploadDT)
</td>
<td class="text-center">
@Html.DisplayFor(modelItem => item.PublicScheduleSize)
</td>
<td class="text-center">
@Html.DisplayFor(modelItem => item.PrivateScheduleSize)
</td>
</tr>
}
</tbody>
</table>
@section Scripts {
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
@page
@model RazorPagesMovie.Pages.Schedules.IndexModel
@{
ViewData["Title"] = "Schedules";
}
<h2>Schedules</h2>
<hr />
<h3>Upload Schedules</h3>
<div class="row">
<div class="col-md-4">
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="FileUpload.Title" class="control-label"></label>
<input asp-for="FileUpload.Title" type="text" class="form-control" />
<span asp-validation-for="FileUpload.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FileUpload.UploadPublicSchedule" class="control-label"></label>
<input asp-for="FileUpload.UploadPublicSchedule" type="file" class="form-control"
style="height:auto" />
<span asp-validation-for="FileUpload.UploadPublicSchedule" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FileUpload.UploadPrivateSchedule" class="control-label"></label>
<input asp-for="FileUpload.UploadPrivateSchedule" type="file" class="form-control"
style="height:auto" />
<span asp-validation-for="FileUpload.UploadPrivateSchedule" class="text-danger"></span>
</div>
<input type="submit" value="Upload" class="btn btn-default" />
</form>
</div>
</div>
<h3>Loaded Schedules</h3>
<table class="table">
<thead>
<tr>
<th></th>
<th>
@Html.DisplayNameFor(model => model.Schedule[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Schedule[0].UploadDT)
</th>
<th class="text-center">
@Html.DisplayNameFor(model => model.Schedule[0].PublicScheduleSize)
</th>
<th class="text-center">
@Html.DisplayNameFor(model => model.Schedule[0].PrivateScheduleSize)
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Schedule) {
<tr>
<td>
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.UploadDT)
</td>
<td class="text-center">
@Html.DisplayFor(modelItem => item.PublicScheduleSize)
</td>
<td class="text-center">
@Html.DisplayFor(modelItem => item.PrivateScheduleSize)
</td>
</tr>
}
</tbody>
</table>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Each form group includes a <label> that displays the name of each class property. The Display attributes in the
FileUpload model provide the display values for the labels. For example, the UploadPublicSchedule property's
display name is set with [Display(Name="Public Schedule")] and thus displays "Public Schedule" in the label when
the form renders.
Each form group includes a validation <span>. If the user's input fails to meet the property attributes set in the
FileUpload class or if any of the ProcessFormFile method file validation checks fail, the model fails to validate.
When model validation fails, a helpful validation message is rendered to the user. For example, the Title property
is annotated with [Required] and [StringLength(60, MinimumLength = 3)] . If the user fails to supply a title, they
receive a message indicating that a value is required. If the user enters a value less than three characters or more
than sixty characters, they receive a message indicating that the value has an incorrect length. If a file is provided
that has no content, a message appears indicating that the file is empty.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using RazorPagesMovie.Utilities;
namespace RazorPagesMovie.Pages.Schedules
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public FileUpload FileUpload { get; set; }
var publicScheduleData =
await FileHelpers.ProcessFormFile(FileUpload.UploadPublicSchedule, ModelState);
var privateScheduleData =
await FileHelpers.ProcessFormFile(FileUpload.UploadPrivateSchedule, ModelState);
_context.Schedule.Add(schedule);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using RazorPagesMovie.Utilities;
namespace RazorPagesMovie.Pages.Schedules
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public FileUpload FileUpload { get; set; }
public IList<Schedule> Schedule { get; private set; }
var publicScheduleData =
await FileHelpers.ProcessFormFile(FileUpload.UploadPublicSchedule, ModelState);
var privateScheduleData =
await FileHelpers.ProcessFormFile(FileUpload.UploadPrivateSchedule, ModelState);
_context.Schedule.Add(schedule);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
[BindProperty]
public FileUpload FileUpload { get; set; }
[BindProperty]
public FileUpload FileUpload { get; set; }
The model also uses a list of the schedules ( IList<Schedule> ) to display the schedules stored in the database on
the page:
When the page loads with OnGetAsync , Schedules is populated from the database and used to generate an HTML
table of loaded schedules:
When the form is posted to the server, the ModelState is checked. If invalid, Schedule is rebuilt, and the page
renders with one or more validation messages stating why page validation failed. If valid, the FileUpload
properties are used in OnPostAsync to complete the file upload for the two versions of the schedule and to create a
new Schedule object to store the data. The schedule is then saved to the database:
var publicScheduleData =
await FileHelpers.ProcessSchedule(FileUpload.UploadPublicSchedule, ModelState);
var privateScheduleData =
await FileHelpers.ProcessSchedule(FileUpload.UploadPrivateSchedule, ModelState);
_context.Schedule.Add(schedule);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
public async Task<IActionResult> OnPostAsync()
{
// Perform an initial check to catch FileUpload class
// attribute violations.
if (!ModelState.IsValid)
{
Schedule = await _context.Schedule.AsNoTracking().ToListAsync();
return Page();
}
var publicScheduleData =
await FileHelpers.ProcessSchedule(FileUpload.UploadPublicSchedule, ModelState);
var privateScheduleData =
await FileHelpers.ProcessSchedule(FileUpload.UploadPrivateSchedule, ModelState);
_context.Schedule.Add(schedule);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
@{
ViewData["Title"] = "Delete Schedule";
}
<h2>Delete Schedule</h2>
<form method="post">
<input type="hidden" asp-for="Schedule.ID" />
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
@page "{id:int}"
@model RazorPagesMovie.Pages.Schedules.DeleteModel
@{
ViewData["Title"] = "Delete Schedule";
}
<h2>Delete Schedule</h2>
<form method="post">
<input type="hidden" asp-for="Schedule.ID" />
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
The page model (Delete.cshtml.cs) loads a single schedule identified by id in the request's route data. Add the
Delete.cshtml.cs file to the Schedules folder:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Schedules
{
public class DeleteModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
[BindProperty]
public Schedule Schedule { get; set; }
if (Schedule == null)
{
return NotFound();
}
return Page();
}
if (Schedule != null)
{
_context.Schedule.Remove(Schedule);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Schedules
{
public class DeleteModel : PageModel
{
private readonly RazorPagesMovie.Models.MovieContext _context;
[BindProperty]
public Schedule Schedule { get; set; }
if (Schedule == null)
{
return NotFound();
}
return Page();
}
if (Schedule != null)
{
_context.Schedule.Remove(Schedule);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
}
}
if (Schedule != null)
{
_context.Schedule.Remove(Schedule);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
if (Schedule != null)
{
_context.Schedule.Remove(Schedule);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
After successfully deleting the schedule, the RedirectToPage sends the user back to the schedules Index.cshtml
page.
Type two letters into the Title field. The validation message changes to indicate that the title must be between 3-60
characters:
When one or more schedules are uploaded, the Loaded Schedules section renders the loaded schedules:
The user can click the Delete link from there to reach the delete confirmation view, where they have an
opportunity to confirm or cancel the delete operation.
Troubleshooting
For troubleshooting information with IFormFile uploading, see the File uploads in ASP.NET Core:
Troubleshooting.
ASP.NET Core Razor SDK
6/21/2018 • 3 minutes to read • Edit Online
By Rick Anderson
The .NET Core 2.1 SDK or later includes the Microsoft.NET.Sdk.Razor MSBuild SDK (Razor SDK). The Razor SDK:
Standardizes the experience around building, packaging, and publishing projects containing Razor files for
ASP.NET Core MVC -based projects.
Includes a set of predefined targets, properties, and items that allow customizing the compilation of Razor files.
Prerequisites
.NET Core 2.1 SDK or later
<Project SDK="Microsoft.NET.Sdk.Razor">
...
</Project>
The preceding packages are included in Microsoft.AspNetCore.Mvc . The following markup shows a basic .csproj file
that uses the Razor SDK to build Razor files for an ASP.NET Core Razor Pages app:
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.0" />
</ItemGroup>
</Project>
Properties
The following properties control the Razor's SDK behavior as part of a project build:
RazorCompileOnBuild : When true , compiles and emits the Razor assembly as part of building the project.
Defaults to true .
RazorCompileOnPublish : When true , compiles and emits the Razor assembly as part of publishing the project.
Defaults to true .
The following properties and items are used to configure inputs and output to the Razor SDK:
ITEMS DESCRIPTION
RazorCompile Item elements (.cs files) that are inputs to Razor compilation
targets. Use this ItemGroup to specify additional files to be
compiled into the Razor assembly.
RazorTargetAssemblyAttribute Item elements used to code generate attributes for the Razor
assembly. For example:
<RazorAssemblyAttribute
Include="System.Reflection.AssemblyMetadataAttribute"
_Parameter1="BuildSource"
_Parameter2="https://docs.asp.net/">
PROPERTY DESCRIPTION
EnableDefaultContentItems When true , includes certain file types, such as .cshtml files,
as content in the project. When referenced via
Microsoft.NET.Sdk.Web, also includes all files under wwwroot,
and config files.
IncludeRazorContentInPack When true , all Razor content items (.cshtml files) will be
marked for inclusion in the generated NuGet package.
Defaults to false .
Targets
The Razor SDK defines two primary targets:
RazorGenerate - Code generates .cs files from RazorGenerate item elements. Use RazorGenerateDependsOn
property to specify additional targets that can run before or after this target.
RazorCompile - Compiles generated .cs files in to a Razor assembly. Use RazorCompileDependsOn to specify
additional targets that can run before or after this target.
Runtime compilation of Razor views
By default, the Razor SDK doesn't publish reference assemblies that are required to perform runtime
compilation. This results in compilation failures when the application model relies on runtime compilation—
for example, the app uses embedded views or changes views after the app is published. Set
CopyRefAssembliesToPublishDirectory to true to continue publishing reference assemblies.
By Steve Smith
ASP.NET Core MVC is a rich framework for building web apps and APIs using the Model-View -Controller
design pattern.
This delineation of responsibilities helps you scale the application in terms of complexity because it's easier to
code, debug, and test something (model, view, or controller) that has a single job (and follows the Single
Responsibility Principle). It's more difficult to update, test, and debug code that has dependencies spread across
two or more of these three areas. For example, user interface logic tends to change more frequently than
business logic. If presentation code and business logic are combined in a single object, an object containing
business logic must be modified every time the user interface is changed. This often introduces errors and
requires the retesting of business logic after every minimal user interface change.
NOTE
Both the view and the controller depend on the model. However, the model depends on neither the view nor the
controller. This is one of the key benefits of the separation. This separation allows the model to be built and tested
independent of the visual presentation.
Model Responsibilities
The Model in an MVC application represents the state of the application and any business logic or operations
that should be performed by it. Business logic should be encapsulated in the model, along with any
implementation logic for persisting the state of the application. Strongly-typed views typically use ViewModel
types designed to contain the data to display on that view. The controller creates and populates these ViewModel
instances from the model.
NOTE
There are many ways to organize the model in an app that uses the MVC architectural pattern. Learn more about some
different kinds of model types.
View Responsibilities
Views are responsible for presenting content through the user interface. They use the Razor view engine to
embed .NET code in HTML markup. There should be minimal logic within views, and any logic in them should
relate to presenting content. If you find the need to perform a great deal of logic in view files in order to display
data from a complex model, consider using a View Component, ViewModel, or view template to simplify the
view.
Controller Responsibilities
Controllers are the components that handle user interaction, work with the model, and ultimately select a view to
render. In an MVC application, the view only displays information; the controller handles and responds to user
input and interaction. In the MVC pattern, the controller is the initial entry point, and is responsible for selecting
which model types to work with and which view to render (hence its name - it controls how the app responds to
a given request).
NOTE
Controllers shouldn't be overly complicated by too many responsibilities. To keep controller logic from becoming overly
complex, use the Single Responsibility Principle to push business logic out of the controller and into the domain model.
TIP
If you find that your controller actions frequently perform the same kinds of actions, you can follow the Don't Repeat
Yourself principle by moving these common actions into filters.
Features
ASP.NET Core MVC includes the following:
Routing
Model binding
Model validation
Dependency injection
Filters
Areas
Web APIs
Testability
Razor view engine
Strongly typed views
Tag Helpers
View Components
Routing
ASP.NET Core MVC is built on top of ASP.NET Core's routing, a powerful URL -mapping component that lets
you build applications that have comprehensible and searchable URLs. This enables you to define your
application's URL naming patterns that work well for search engine optimization (SEO ) and for link generation,
without regard for how the files on your web server are organized. You can define your routes using a
convenient route template syntax that supports route value constraints, defaults and optional values.
Convention-based routing enables you to globally define the URL formats that your application accepts and how
each of those formats maps to a specific action method on given controller. When an incoming request is
received, the routing engine parses the URL and matches it to one of the defined URL formats, and then calls the
associated controller's action method.
Attribute routing enables you to specify routing information by decorating your controllers and actions with
attributes that define your application's routes. This means that your route definitions are placed next to the
controller and action with which they're associated.
[Route("api/[controller]")]
public class ProductsController : Controller
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
...
}
}
Model binding
ASP.NET Core MVC model binding converts client request data (form values, route data, query string
parameters, HTTP headers) into objects that the controller can handle. As a result, your controller logic doesn't
have to do the work of figuring out the incoming request data; it simply has the data as parameters to its action
methods.
Model validation
ASP.NET Core MVC supports validation by decorating your model object with data annotation validation
attributes. The validation attributes are checked on the client side before values are posted to the server, as well
as on the server before the controller action is called.
using System.ComponentModel.DataAnnotations;
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
A controller action:
The framework handles validating request data both on the client and on the server. Validation logic specified on
model types is added to the rendered views as unobtrusive annotations and is enforced in the browser with
jQuery Validation.
Dependency injection
ASP.NET Core has built-in support for dependency injection (DI). In ASP.NET Core MVC, controllers can
request needed services through their constructors, allowing them to follow the Explicit Dependencies Principle.
Your app can also use dependency injection in view files, using the @inject directive:
Filters
Filters help developers encapsulate cross-cutting concerns, like exception handling or authorization. Filters
enable running custom pre- and post-processing logic for action methods, and can be configured to run at
certain points within the execution pipeline for a given request. Filters can be applied to controllers or actions as
attributes (or can be run globally). Several filters (such as Authorize ) are included in the framework.
[Authorize] is the attribute that is used to create MVC authorization filters.
[Authorize]
public class AccountController : Controller
{
Areas
Areas provide a way to partition a large ASP.NET Core MVC Web app into smaller functional groupings. An area
is an MVC structure inside an application. In an MVC project, logical components like Model, Controller, and
View are kept in different folders, and MVC uses naming conventions to create the relationship between these
components. For a large app, it may be advantageous to partition the app into separate high level areas of
functionality. For instance, an e-commerce app with multiple business units, such as checkout, billing, and search
etc. Each of these units have their own logical component views, controllers, and models.
Web APIs
In addition to being a great platform for building web sites, ASP.NET Core MVC has great support for building
Web APIs. You can build services that reach a broad range of clients including browsers and mobile devices.
The framework includes support for HTTP content-negotiation with built-in support to format data as JSON or
XML. Write custom formatters to add support for your own formats.
Use link generation to enable support for hypermedia. Easily enable support for cross-origin resource sharing
(CORS ) so that your Web APIs can be shared across multiple Web applications.
Testability
The framework's use of interfaces and dependency injection make it well-suited to unit testing, and the
framework includes features (like a TestHost and InMemory provider for Entity Framework) that make
integration tests quick and easy as well. Learn more about how to test controller logic.
Razor view engine
ASP.NET Core MVC views use the Razor view engine to render views. Razor is a compact, expressive and fluid
template markup language for defining views using embedded C# code. Razor is used to dynamically generate
web content on the server. You can cleanly mix server code with client side content and code.
<ul>
@for (int i = 0; i < 5; i++) {
<li>List item @i</li>
}
</ul>
Using the Razor view engine you can define layouts, partial views and replaceable sections.
Strongly typed views
Razor views in MVC can be strongly typed based on your model. Controllers can pass a strongly typed model to
views enabling your views to have type checking and IntelliSense support.
For example, the following view renders a model of type IEnumerable<Product> :
@model IEnumerable<Product>
<ul>
@foreach (Product p in Model)
{
<li>@p.Name</li>
}
</ul>
Tag Helpers
Tag Helpers enable server side code to participate in creating and rendering HTML elements in Razor files. You
can use tag helpers to define custom tags (for example, <environment> ) or to modify the behavior of existing tags
(for example, <label> ). Tag Helpers bind to specific elements based on the element name and its attributes. They
provide the benefits of server-side rendering while still preserving an HTML editing experience.
There are many built-in Tag Helpers for common tasks - such as creating forms, links, loading assets and more -
and even more available in public GitHub repositories and as NuGet packages. Tag Helpers are authored in C#,
and they target HTML elements based on element name, attribute name, or parent tag. For example, the built-in
LinkTagHelper can be used to create a link to the Login action of the AccountsController :
<p>
Thank you for confirming your email.
Please <a asp-controller="Account" asp-action="Login">Click here to Log in</a>.
</p>
The EnvironmentTagHelper can be used to include different scripts in your views (for example, raw or minified)
based on the runtime environment, such as Development, Staging, or Production:
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
</environment>
Tag Helpers provide an HTML -friendly development experience and a rich IntelliSense environment for creating
HTML and Razor markup. Most of the built-in Tag Helpers target existing HTML elements and provide server-
side attributes for the element.
View Components
View Components allow you to package rendering logic and reuse it throughout the application. They're similar
to partial views, but with associated logic.
Compatibility version
The SetCompatibilityVersion method allows an app to opt-in or opt-out of potentially breaking behavior changes
introduced in ASP.NET Core MVC 2.1 or later.
For more information, see Compatibility version for ASP.NET Core MVC.
Model Binding in ASP.NET Core
8/16/2018 • 7 minutes to read • Edit Online
By Rachel Appel
Since the route template looks like this, {controller=Home}/{action=Index}/{id?} , movies/edit/2 routes to
the Movies controller, and its Edit action method. It also accepts an optional parameter called id . The
code for the action method should look something like this:
Note: The strings in the URL route are not case sensitive.
MVC will try to bind request data to the action parameters by name. MVC will look for values for each
parameter using the parameter name and the names of its public settable properties. In the above
example, the only action parameter is named id , which MVC binds to the value with the same name in
the route values. In addition to route values MVC will bind data from various parts of the request and it
does so in a set order. Below is a list of the data sources in the order that model binding looks through
them:
1. Form values : These are form values that go in the HTTP request using the POST method.
(including jQuery POST requests).
2. Route values : The set of route values provided by Routing
3. Query strings : The query string part of the URI.
Note: Form values, route data, and query strings are all stored as name-value pairs.
Since model binding asked for a key named id and there's nothing named id in the form values, it
moved on to the route values looking for that key. In our example, it's a match. Binding happens, and the
value is converted to the integer 2. The same request using Edit(string id) would convert to the string "2".
So far the example uses simple types. In MVC simple types are any .NET primitive type or type with a
string type converter. If the action method's parameter were a class such as the Movie type, which
contains both simple and complex types as properties, MVC's model binding will still handle it nicely. It
uses reflection and recursion to traverse the properties of complex types looking for matches. Model
binding looks for the pattern parameter_name.property_name to bind values to properties. If it doesn't
find matching values of this form, it will attempt to bind using just the property name. For those types
such as Collection types, model binding looks for matches to parameter_name[index] or just [index].
Model binding treats Dictionary types similarly, asking for parameter_name[key ] or just [key ], as long as
the keys are simple types. Keys that are supported match the field names HTML and tag helpers
generated for the same model type. This enables round-tripping values so that the form fields remain
filled with the user's input for their convenience, for example, when bound data from a create or edit
didn't pass validation.
In order for binding to happen the class must have a public default constructor and member to be bound
must be public writable properties. When model binding happens the class will only be instantiated using
the public default constructor, then the properties can be set.
When a parameter is bound, model binding stops looking for values with that name and it moves on to
bind the next parameter. Otherwise, the default model binding behavior sets parameters to their default
values depending on their type:
T[] : With the exception of arrays of type byte[] , binding sets parameters of type T[] to
Array.Empty<T>() . Arrays of type byte[] are set to null .
Reference Types: Binding creates an instance of a class with the default constructor without setting
properties. However, model binding sets string parameters to null .
Nullable Types: Nullable types are set to null . In the above example, model binding sets id to
null since it's of type int? .
Value Types: Non-nullable value types of type T are set to default(T) . For example, model
binding will set a parameter int id to 0. Consider using model validation or nullable types rather
than relying on default values.
If binding fails, MVC doesn't throw an error. Every action which accepts user input should check the
ModelState.IsValid property.
Note: Each entry in the controller's ModelState property is a ModelStateEntry containing an Errors
property. It's rarely necessary to query this collection yourself. Use ModelState.IsValid instead.
Additionally, there are some special data types that MVC must consider when performing model binding:
IFormFile , IEnumerable<IFormFile> : One or more uploaded files that are part of the HTTP request.
CancellationToken : Used to cancel activity in asynchronous controllers.
These types can be bound to action parameters or to properties on a class type.
Once model binding is complete, Validation occurs. Default model binding works great for the vast
majority of development scenarios. It's also extensible so if you have unique needs you can customize the
built-in behavior.
services.AddMvc().AddMvcOptions(options =>
options.ModelMetadataDetailsProviders.Add(
new ExcludeBindingMetadataProvider(typeof(System.Version))));
services.AddMvc().AddMvcOptions(options =>
options.ModelMetadataDetailsProviders.Add(
new SuppressChildValidationMetadataProvider(typeof(System.Guid))));
NOTE
There can be at most one parameter per action decorated with [FromBody] . The ASP.NET Core MVC run-time
delegates the responsibility of reading the request stream to the formatter. Once the request stream is read for a
parameter, it's generally not possible to read the request stream again for binding other [FromBody] parameters.
NOTE
The JsonInputFormatter is the default formatter and is based on Json.NET.
ASP.NET Core selects input formatters based on the Content-Type header and the type of the parameter,
unless there's an attribute applied to it specifying otherwise. If you'd like to use XML or another format
you must configure it in the Startup.cs file, but you may first have to obtain a reference to
Microsoft.AspNetCore.Mvc.Formatters.Xml using NuGet. Your startup code should look something like this:
Code in the Startup.cs file contains a ConfigureServices method with a services argument you can use
to build up services for your ASP.NET Core app. In the sample, we are adding an XML formatter as a
service that MVC will provide for this app. The options argument passed into the AddMvc method allows
you to add and manage filters, formatters, and other system options from MVC upon app startup. Then
apply the Consumes attribute to controller classes or action methods to work with the format you want.
Custom Model Binding
You can extend model binding by writing your own custom model binders. Learn more about custom
model binding.
Model validation in ASP.NET Core MVC
8/30/2018 • 15 minutes to read • Edit Online
By Rachel Appel
Validation Attributes
Validation attributes are a way to configure model validation so it's similar conceptually to validation on
fields in database tables. This includes constraints such as assigning data types or required fields. Other
types of validation include applying patterns to data to enforce business rules, such as a credit card, phone
number, or email address. Validation attributes make enforcing these requirements much simpler and
easier to use.
Below is an annotated Movie model from an app that stores information about movies and TV shows.
Most of the properties are required and several string properties have length requirements. Additionally,
there's a numeric range restriction in place for the Price property from 0 to $999.99, along with a custom
validation attribute.
[Required]
[StringLength(100)]
public string Title { get; set; }
[ClassicMovie(1960)]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
[Required]
public Genre Genre { get; set; }
Model State
Model state represents validation errors in submitted HTML form values.
MVC will continue validating fields until reaches the maximum number of errors (200 by default). You can
configure this number by inserting the following code into the ConfigureServices method in the Startup.cs
file:
Manual validation
After model binding and validation are complete, you may want to repeat parts of it. For example, a user
may have entered text in a field expecting an integer, or you may need to compute a value for a model's
property.
You may need to run validation manually. To do so, call the TryValidateModel method, as shown here:
TryValidateModel(movie);
Custom validation
Validation attributes work for most validation needs. However, some validation rules are specific to your
business. Your rules might not be common data validation techniques such as ensuring a field is required
or that it conforms to a range of values. For these scenarios, custom validation attributes are a great
solution. Creating your own custom validation attributes in MVC is easy. Just inherit from the
ValidationAttribute , and override the IsValid method. The IsValid method accepts two parameters,
the first is an object named value and the second is a ValidationContext object named validationContext.
Value refers to the actual value from the field that your custom validator is validating.
In the following sample, a business rule states that users may not set the genre to Classic for a movie
released after 1960. The [ClassicMovie] attribute checks the genre first, and if it's a classic, then it checks
the release date to see that it's later than 1960. If it's released after 1960, validation fails. The attribute
accepts an integer parameter representing the year that you can use to validate data. You can capture the
value of the parameter in the attribute's constructor, as shown here:
public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator
{
private int _year;
return ValidationResult.Success;
}
The movie variable above represents a Movie object that contains the data from the form submission to
validate. In this case, the validation code checks the date and genre in the IsValid method of the
ClassicMovieAttribute class as per the rules. Upon successful validation , IsValid returns a
ValidationResult.Success code. When validation fails, a ValidationResult with an error message is
returned:
When a user modifies the Genre field and submits the form, the IsValid method of the
ClassicMovieAttribute will verify whether the movie is a classic. Like any built-in attribute, apply the
ClassicMovieAttribute to a property such as ReleaseDate to ensure validation happens, as shown in the
previous code sample. Since the example works only with Movie types, a better option is to use
IValidatableObject as shown in the following paragraph.
Alternatively, this same code could be placed in the model by implementing the Validate method on the
IValidatableObject interface. While custom validation attributes work well for validating individual
properties, implementing IValidatableObject can be used to implement class-level validation as seen here.
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.0.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js"></script>
<script
src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.mi
n.js"></script>
The jQuery Unobtrusive Validation script is a custom Microsoft front-end library that builds on the popular
jQuery Validate plugin. Without jQuery Unobtrusive Validation, you would have to code the same
validation logic in two places: once in the server side validation attributes on model properties, and then
again in client side scripts (the examples for jQuery Validate's validate() method shows how complex this
could become). Instead, MVC's Tag Helpers and HTML helpers are able to use the validation attributes and
type metadata from model properties to render HTML 5 data- attributes in the form elements that need
validation. MVC generates the data- attributes for both built-in and custom attributes. jQuery
Unobtrusive Validation then parses the data- attributes and passes the logic to jQuery Validate, effectively
"copying" the server side validation logic to the client. You can display validation errors on the client using
the relevant tag helpers as shown here:
<div class="form-group">
<label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
</div>
The tag helpers above render the HTML below. Notice that the data- attributes in the HTML output
correspond to the validation attributes for the ReleaseDate property. The data-val-required attribute
below contains an error message to display if the user doesn't fill in the release date field. jQuery
Unobtrusive Validation passes this value to the jQuery Validate required() method, which then displays
that message in the accompanying <span> element.
Client-side validation prevents submission until the form is valid. The Submit button runs JavaScript that
either submits the form or displays error messages.
MVC determines type attribute values based on the .NET data type of a property, possibly overridden
using [DataType] attributes. The base [DataType] attribute does no real server-side validation. Browsers
choose their own error messages and display those errors as they wish, however the jQuery Validation
Unobtrusive package can override the messages and display them consistently with others. This happens
most obviously when users apply [DataType] subclasses such as [EmailAddress] .
Add Validation to Dynamic Forms
Because jQuery Unobtrusive Validation passes validation logic and parameters to jQuery Validate when the
page first loads, dynamically generated forms won't automatically exhibit validation. Instead, you must tell
jQuery Unobtrusive Validation to parse the dynamic form immediately after creating it. For example, the
code below shows how you might set up client side validation on a form added via AJAX.
$.get({
url: "https://url/that/returns/a/form",
dataType: "html",
error: function(jqXHR, textStatus, errorThrown) {
alert(textStatus + ": Couldn't add form. " + errorThrown);
},
success: function(newFormHTML) {
var container = document.getElementById("form-container");
container.insertAdjacentHTML("beforeend", newFormHTML);
var forms = container.getElementsByTagName("form");
var newForm = forms[forms.length - 1];
$.validator.unobtrusive.parse(newForm);
}
})
The $.validator.unobtrusive.parse() method accepts a jQuery selector for its one argument. This method
tells jQuery Unobtrusive Validation to parse the data- attributes of forms within that selector. The values
of those attributes are then passed to the jQuery Validate plugin so that the form exhibits the desired client
side validation rules.
Add Validation to Dynamic Controls
You can also update the validation rules on a form when individual controls, such as <input/> s and
<select/> s, are dynamically generated. You cannot pass selectors for these elements to the parse()
method directly because the surrounding form has already been parsed and won't update. Instead, you first
remove the existing validation data, then reparse the entire form, as shown below:
$.get({
url: "https://url/that/returns/a/control",
dataType: "html",
error: function(jqXHR, textStatus, errorThrown) {
alert(textStatus + ": Couldn't add control. " + errorThrown);
},
success: function(newInputHTML) {
var form = document.getElementById("my-form");
form.insertAdjacentHTML("beforeend", newInputHTML);
$(form).removeData("validator") // Added by jQuery Validate
.removeData("unobtrusiveValidation"); // Added by jQuery Unobtrusive Validation
$.validator.unobtrusive.parse(form);
}
})
IClientModelValidator
You may create client side logic for your custom attribute, and unobtrusive validation which creates an
adapter to jquery validation will execute it on the client for you automatically as part of validation. The first
step is to control what data- attributes are added by implementing the IClientModelValidator interface as
shown here:
Attributes that implement this interface can add HTML attributes to generated fields. Examining the output
for the ReleaseDate element reveals HTML that's similar to the previous example, except now there's a
data-val-classicmovie attribute that was defined in the AddValidation method of IClientModelValidator .
Unobtrusive validation uses the data in the data- attributes to display error messages. However, jQuery
doesn't know about rules or messages until you add them to jQuery's validator object. This is shown in
the following example, which adds a custom classicmovie client validation method to the jQuery
validator object. For an explanation of the unobtrusive.adapters.add method, see Unobtrusive Client
Validation in ASP.NET MVC.
$.validator.addMethod('classicmovie',
function (value, element, params) {
// Get element value. Classic genre has value '0'.
var genre = $(params[0]).val(),
year = params[1],
date = new Date(value);
if (genre && genre.length > 0 && genre[0] === '0') {
// Since this is a classic movie, invalid if release date is after given year.
return date.getFullYear() <= year;
}
return true;
});
$.validator.unobtrusive.adapters.add('classicmovie',
['year'],
function (options) {
var element = $(options.form).find('select#Genre')[0];
options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
options.messages['classicmovie'] = options.message;
});
With the preceding code, the classicmovie method performs client-side validation on the movie release
date. The error message displays if the method returns false .
Remote validation
Remote validation is a great feature to use when you need to validate data on the client against data on the
server. For example, your app may need to verify whether an email or user name is already in use, and it
must query a large amount of data to do so. Downloading large sets of data for validating one or a few
fields consumes too many resources. It may also expose sensitive information. An alternative is to make a
round-trip request to validate a field.
You can implement remote validation in a two step process. First, you must annotate your model with the
[Remote] attribute. The [Remote] attribute accepts multiple overloads you can use to direct client side
JavaScript to the appropriate code to call. The example below points to the VerifyEmail action method of
the Users controller.
The second step is putting the validation code in the corresponding action method as defined in the
[Remote] attribute. According to the jQuery Validate remote method documentation, the server response
must be a JSON string that's either:
"true" for valid elements.
"false" , undefined , or null for invalid elements, using the default error message.
If the server response is a string (for example, "That name is already taken, try peter123 instead" ), the
string is displayed as a custom error message in place of the default string.
The definition of the VerifyEmail method follows these rules, as shown below. It returns a validation error
message if the email is taken, or true if the email is free, and wraps the result in a JsonResult object. The
client side can then use the returned value to proceed or display the error if needed.
[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
{
if (!_userRepository.VerifyEmail(email))
{
return Json($"Email {email} is already in use.");
}
return Json(true);
}
Now when users enter an email, JavaScript in the view makes a remote call to see if that email has been
taken and, if so, displays the error message. Otherwise, the user can submit the form as usual.
The AdditionalFields property of the [Remote] attribute is useful for validating combinations of fields
against data on the server. For example, if the User model from above had two additional properties
called FirstName and LastName , you might want to verify that no existing users already have that pair of
names. You define the new properties as shown in the following code:
AdditionalFields could've been set explicitly to the strings "FirstName" and "LastName" , but using the
nameof operator like this simplifies later refactoring. The action method to perform the validation must
then accept two arguments, one for the value of FirstName and one for the value of LastName .
[AcceptVerbs("Get", "Post")]
public IActionResult VerifyName(string firstName, string lastName)
{
if (!_userRepository.VerifyName(firstName, lastName))
{
return Json(data: $"A user named {firstName} {lastName} already exists.");
}
AdditionalFields , like all attribute arguments, must be a constant expression. Therefore, you must not use
an interpolated string or call string.Join() to initialize AdditionalFields . For every additional field that
you add to the [Remote] attribute, you must add another argument to the corresponding controller action
method.
Views in ASP.NET Core MVC
7/24/2018 • 13 minutes to read • Edit Online
The Home controller is represented by a Home folder inside the Views folder. The Home folder contains the
views for the About, Contact, and Index (homepage) webpages. When a user requests one of these three
webpages, controller actions in the Home controller determine which of the three views is used to build and
return a webpage to the user.
Use layouts to provide consistent webpage sections and reduce code repetition. Layouts often contain the header,
navigation and menu elements, and the footer. The header and footer usually contain boilerplate markup for
many metadata elements and links to script and style assets. Layouts help you avoid this boilerplate markup in
your views.
Partial views reduce code duplication by managing reusable parts of views. For example, a partial view is useful
for an author biography on a blog website that appears in several views. An author biography is ordinary view
content and doesn't require code to execute in order to produce the content for the webpage. Author biography
content is available to the view by model binding alone, so using a partial view for this type of content is ideal.
View components are similar to partial views in that they allow you to reduce repetitive code, but they're
appropriate for view content that requires code to run on the server in order to render the webpage. View
components are useful when the rendered content requires database interaction, such as for a website shopping
cart. View components aren't limited to model binding in order to produce webpage output.
Creating a view
Views that are specific to a controller are created in the Views/[ControllerName] folder. Views that are shared
among controllers are placed in the Views/Shared folder. To create a view, add a new file and give it the same
name as its associated controller action with the .cshtml file extension. To create a view that corresponds with the
About action in the Home controller, create an About.cshtml file in the Views/Home folder:
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
Razor markup starts with the @ symbol. Run C# statements by placing C# code within Razor code blocks set off
by curly braces ( { ... } ). For example, see the assignment of "About" to ViewData["Title"] shown above. You
can display values within HTML by simply referencing the value with the @ symbol. See the contents of the
<h2> and <h3> elements above.
The view content shown above is only part of the entire webpage that's rendered to the user. The rest of the
page's layout and other common aspects of the view are specified in other view files. To learn more, see the
Layout topic.
return View();
}
When this action returns, the About.cshtml view shown in the last section is rendered as the following webpage:
The View helper method has several overloads. You can optionally specify:
An explicit view to return:
return View("Orders");
return View(Orders);
View discovery
When an action returns a view, a process called view discovery takes place. This process determines which view
file is used based on the view name.
The default behavior of the View method ( return View(); ) is to return a view with the same name as the action
method from which it's called. For example, the About ActionResult method name of the controller is used to
search for a view file named About.cshtml. First, the runtime looks in the Views/[ControllerName] folder for the
view. If it doesn't find a matching view there, it searches the Shared folder for the view.
It doesn't matter if you implicitly return the ViewResult with return View(); or explicitly pass the view name to
the View method with return View("<ViewName>"); . In both cases, view discovery searches for a matching view
file in this order:
1. Views/[ControllerName]/[ViewName].cshtml
2. Views/Shared/[ViewName].cshtml
A view file path can be provided instead of a view name. If using an absolute path starting at the app root
(optionally starting with "/" or "~/"), the .cshtml extension must be specified:
return View("Views/Home/About.cshtml");
You can also use a relative path to specify views in different directories without the .cshtml extension. Inside the
HomeController , you can return the Index view of your Manage views with a relative path:
return View("../Manage/Index");
Similarly, you can indicate the current controller-specific directory with the "./" prefix:
return View("./About");
Partial views and view components use similar (but not identical) discovery mechanisms.
You can customize the default convention for how views are located within the app by using a custom
IViewLocationExpander.
View discovery relies on finding view files by file name. If the underlying file system is case sensitive, view names
are probably case sensitive. For compatibility across operating systems, match case between controller and action
names and associated view folders and file names. If you encounter an error that a view file can't be found while
working with a case-sensitive file system, confirm that the casing matches between the requested view file and
the actual view file name.
Follow the best practice of organizing the file structure for your views to reflect the relationships among
controllers, actions, and views for maintainability and clarity.
Visual Studio and Visual Studio Code list strongly typed class members using a feature called IntelliSense. When
you want to see the properties of a viewmodel, type the variable name for the viewmodel followed by a period (
. ). This helps you write code faster with fewer errors.
Specify a model using the @model directive. Use the model with @Model :
@model WebApplication1.ViewModels.Address
<h2>Contact</h2>
<address>
@Model.Street<br>
@Model.City, @Model.State @Model.PostalCode<br>
<abbr title="Phone">P:</abbr> 425.555.0100
</address>
return View(viewModel);
}
There are no restrictions on the model types that you can provide to a view. We recommend using Plain Old CLR
Object (POCO ) viewmodels with little or no behavior (methods) defined. Usually, viewmodel classes are either
stored in the Models folder or a separate ViewModels folder at the root of the app. The Address viewmodel used
in the example above is a POCO viewmodel stored in a file named Address.cs:
namespace WebApplication1.ViewModels
{
public class Address
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
}
}
Nothing prevents you from using the same classes for both your viewmodel types and your business model
types. However, using separate models allows your views to vary independently from the business logic and data
access parts of your app. Separation of models and viewmodels also offers security benefits when models use
model binding and validation for data sent to the app by the user.
Weakly typed data (ViewData, ViewData attribute, and ViewBag)
ViewBag isn't available in Razor Pages.
In addition to strongly typed views, views have access to a weakly typed (also called loosely typed) collection of
data. Unlike strong types, weak types (or loose types) means that you don't explicitly declare the type of data
you're using. You can use the collection of weakly typed data for passing small amounts of data in and out of
controllers and views.
View and a layout view Setting the <title> element content in the layout view from a
view file.
Partial view and a view A widget that displays data based on the webpage that the
user requested.
This collection can be referenced through either the ViewData or ViewBag properties on controllers and views.
The ViewData property is a dictionary of weakly typed objects. The ViewBag property is a wrapper around
ViewData that provides dynamic properties for the underlying ViewData collection.
ViewData and ViewBag are dynamically resolved at runtime. Since they don't offer compile-time type checking,
both are generally more error-prone than using a viewmodel. For that reason, some developers prefer to
minimally or never use ViewData and ViewBag .
ViewData
ViewData is a ViewDataDictionary object accessed through string keys. String data can be stored and used
directly without the need for a cast, but you must cast other ViewData object values to specific types when you
extract them. You can use ViewData to pass data from controllers to views and within views, including partial
views and layouts.
The following is an example that sets values for a greeting and an address using ViewData in an action:
return View();
}
@{
// Since Address isn't a string, it requires a cast.
var address = ViewData["Address"] as Address;
}
@ViewData["Greeting"] World!
<address>
@address.Name<br>
@address.Street<br>
@address.City, @address.State @address.PostalCode
</address>
ViewData attribute
Another approach that uses the ViewDataDictionary is ViewDataAttribute. Properties on controllers or Razor
Page models decorated with [ViewData] have their values stored and loaded from the dictionary.
In the following example, the Home controller contains a Title property decorated with [ViewData] . The About
method sets the title for the About view:
public class HomeController : Controller
{
[ViewData]
public string Title { get; set; }
return View();
}
}
<h1>@Model.Title</h1>
<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"] - WebApplication</title>
...
ViewBag
ViewBag isn't available in Razor Pages.
ViewBag is a DynamicViewData object that provides dynamic access to the objects stored in ViewData . ViewBag
can be more convenient to work with, since it doesn't require casting. The following example shows how to use
ViewBag with the same result as using ViewData above:
return View();
}
@ViewBag.Greeting World!
<address>
@ViewBag.Address.Name<br>
@ViewBag.Address.Street<br>
@ViewBag.Address.City, @ViewBag.Address.State @ViewBag.Address.PostalCode
</address>
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "About Contoso";
ViewData["Description"] = "Let us tell you about Contoso's philosophy and mission.";
}
Read the properties but reverse the use of ViewData and ViewBag . In the _Layout.cshtml file, obtain the title
using ViewData and obtain the description using ViewBag :
<!DOCTYPE html>
<html lang="en">
<head>
<title>@ViewData["Title"]</title>
<meta name="description" content="@ViewBag.Description">
...
Remember that strings don't require a cast for ViewData . You can use @ViewData["Title"] without casting.
Using both ViewData and ViewBag at the same time works, as does mixing and matching reading and writing the
properties. The following markup is rendered:
<!DOCTYPE html>
<html lang="en">
<head>
<title>About Contoso</title>
<meta name="description" content="Let us tell you about Contoso's philosophy and mission.">
...
<address>
@Model.Street<br>
@Model.City, @Model.State @Model.PostalCode<br>
<abbr title="Phone">P:</abbr> 425.555.0100
</address>
This feature offers flexibility but doesn't offer compilation protection or IntelliSense. If the property doesn't exist,
webpage generation fails at runtime.
Rendering HTML
The default Razor language is HTML. Rendering HTML from Razor markup is no different than rendering HTML
from an HTML file. HTML markup in .cshtml Razor files is rendered by the server unchanged.
Razor syntax
Razor supports C# and uses the @ symbol to transition from HTML to C#. Razor evaluates C# expressions and
renders them in the HTML output.
When an @ symbol is followed by a Razor reserved keyword, it transitions into Razor-specific markup.
Otherwise, it transitions into plain C#.
To escape an @ symbol in Razor markup, use a second @ symbol:
<p>@@Username</p>
<p>@Username</p>
HTML attributes and content containing email addresses don't treat the @ symbol as a transition character. The
email addresses in the following example are untouched by Razor parsing:
<a href="mailto:[email protected]">[email protected]</a>
<p>@DateTime.Now</p>
<p>@DateTime.IsLeapYear(2016)</p>
With the exception of the C# await keyword, implicit expressions must not contain spaces. If the C# statement
has a clear ending, spaces can be intermingled:
Implicit expressions cannot contain C# generics, as the characters inside the brackets ( <> ) are interpreted as an
HTML tag. The following code is not valid:
<p>@GenericMethod<int>()</p>
The preceding code generates a compiler error similar to one of the following:
The "int" element wasn't closed. All elements must be either self-closing or have a matching end tag.
Cannot convert method group 'GenericMethod' to non-delegate type 'object'. Did you intend to invoke the
method?`
Generic method calls must be wrapped in an explicit Razor expression or a Razor code block.
Any content within the @() parenthesis is evaluated and rendered to the output.
Implicit expressions, described in the previous section, generally can't contain spaces. In the following code, one
week isn't subtracted from the current time:
@{
var joe = new Person("Joe", 33);
}
<p>Age@(joe.Age)</p>
Without the explicit expression, <p>[email protected]</p> is treated as an email address, and <p>[email protected]</p> is
rendered. When written as an explicit expression, <p>Age33</p> is rendered.
Explicit expressions can be used to render output from generic methods in .cshtml files. The following markup
shows how to correct the error shown earlier caused by the brackets of a C# generic. The code is written as an
explicit expression:
<p>@(GenericMethod<int>())</p>
Expression encoding
C# expressions that evaluate to a string are HTML encoded. C# expressions that evaluate to IHtmlContent are
rendered directly through IHtmlContent.WriteTo . C# expressions that don't evaluate to IHtmlContent are
converted to a string by ToString and encoded before they're rendered.
@("<span>Hello World</span>")
<span>Hello World</span>
<span>Hello World</span>
WARNING
Using HtmlHelper.Raw on unsanitized user input is a security risk. User input might contain malicious JavaScript or other
exploits. Sanitizing user input is difficult. Avoid using HtmlHelper.Raw with user input.
@Html.Raw("<span>Hello World</span>")
<span>Hello World</span>
@{
var quote = "The future depends on what you do today. - Mahatma Gandhi";
}
<p>@quote</p>
@{
quote = "Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.";
}
<p>@quote</p>
Implicit transitions
The default language in a code block is C#, but the Razor Page can transition back to HTML:
@{
var inCSharp = true;
<p>Now in HTML, was in C# @inCSharp</p>
}
Use this approach to render HTML that isn't surrounded by an HTML tag. Without an HTML or Razor tag, a
Razor runtime error occurs.
The <text> tag is useful to control whitespace when rendering content:
Only the content between the <text> tag is rendered.
No whitespace before or after the <text> tag appears in the HTML output.
Explicit Line Transition with @:
To render the rest of an entire line as HTML inside a code block, use the @: syntax:
Control structures
Control structures are an extension of code blocks. All aspects of code blocks (transitioning to markup, inline C#)
also apply to the following structures:
Conditionals @if, else if, else, and @switch
@if controls when code runs:
@if (value % 2 == 0)
{
<p>The value was even.</p>
}
@switch (value)
{
case 1:
<p>The value is 1!</p>
break;
case 1337:
<p>Your number is 1337!</p>
break;
default:
<p>Your number wasn't 1 or 1337.</p>
break;
}
@{
var people = new Person[]
{
new Person("Weston", 33),
new Person("Johnathon", 41),
...
};
}
@foreach
@{ var i = 0; }
@while (i < people.Length)
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
i++;
}
@do while
@{ var i = 0; }
@do
{
var person = people[i];
<p>Name: @person.Name</p>
<p>Age: @person.Age</p>
i++;
} while (i < people.Length);
Compound @using
In C#, a using statement is used to ensure an object is disposed. In Razor, the same mechanism is used to
create HTML Helpers that contain additional content. In the following code, HTML Helpers render a form tag
with the @using statement:
@using (Html.BeginForm())
{
<div>
email:
<input type="email" id="Email" value="">
<button>Register</button>
</div>
}
@try
{
throw new InvalidOperationException("You did something invalid.");
}
catch (Exception ex)
{
<p>The exception message: @ex.Message</p>
}
finally
{
<p>The finally statement.</p>
}
@lock
Razor has the capability to protect critical sections with lock statements:
@lock (SomeLock)
{
// Do critical section work
}
Comments
Razor supports C# and HTML comments:
@{
/* C# comment */
// Another C# comment
}
<!-- HTML comment -->
Razor comments are removed by the server before the webpage is rendered. Razor uses @* *@ to delimit
comments. The following code is commented out, so the server doesn't render any markup:
@*
@{
/* C# comment */
// Another C# comment
}
<!-- HTML comment -->
*@
Directives
Razor directives are represented by implicit expressions with reserved keywords following the @ symbol. A
directive typically changes the way a view is parsed or enables different functionality.
Understanding how Razor generates code for a view makes it easier to understand how directives work.
@{
var quote = "Getting old ain't for wimps! - Anonymous";
}
@using System.IO
@{
var dir = Directory.GetCurrentDirectory();
}
<p>@dir</p>
@model
The @model directive specifies the type of the model passed to a view:
@model TypeNameOfModel
In an ASP.NET Core MVC app created with individual user accounts, the Views/Account/Login.cshtml view
contains the following model declaration:
@model LoginViewModel
Razor exposes a Model property for accessing the model passed to the view:
The @model directive specifies the type of this property. The directive specifies the T in RazorPage<T> that the
generated class that the view derives from. If the @model directive isn't specified, the Model property is of type
dynamic . The value of the model is passed from the controller to the view. For more information, see Strongly
typed models and the @model keyword.
@inherits
The @inherits directive provides full control of the class the view inherits:
@inherits TypeNameOfClassToInheritFrom
using Microsoft.AspNetCore.Mvc.Razor;
<div>Custom text: Gardyloo! - A Scottish warning yelled from a window before dumping a slop bucket on the
street below.</div>
@model and @inherits can be used in the same view. @inherits can be in a _ViewImports.cshtml file that the
view imports:
@inherits CustomRazorPage<TModel>
@inherits CustomRazorPage<TModel>
If "[email protected]" is passed in the model, the view generates the following HTML markup:
@inject
The @inject directive enables the Razor Page to inject a service from the service container into a view. For more
information, see Dependency injection into views.
@functions
The @functions directive enables a Razor Page to add a C# code block to a view:
@functions { // C# Code }
For example:
@functions {
public string GetHello()
{
return "Hello";
}
}
@section
The @section directive is used in conjunction with the layout to enable views to render content in different parts
of the HTML page. For more information, see Sections.
Tag Helpers
There are three directives that pertain to Tag Helpers.
DIRECTIVE FUNCTION
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
// Look at generatedCode
return csharpDocument;
}
}
Set a break point on the return csharpDocument statement of CustomTemplateEngine . When program execution
stops at the break point, view the value of generatedCode .
View lookups and case sensitivity
The Razor view engine performs case-sensitive lookups for views. However, the actual lookup is determined by
the underlying file system:
File based source:
On operating systems with case insensitive file systems (for example, Windows), physical file provider
lookups are case insensitive. For example, return View("Test") results in matches for
/Views/Home/Test.cshtml, /Views/home/test.cshtml, and any other casing variant.
On case-sensitive file systems (for example, Linux, OSX, and with EmbeddedFileProvider ), lookups are
case-sensitive. For example, return View("Test") specifically matches /Views/Home/Test.cshtml.
Precompiled views: With ASP.NET Core 2.0 and later, looking up precompiled views is case insensitive on all
operating systems. The behavior is identical to physical file provider's behavior on Windows. If two
precompiled views differ only in case, the result of lookup is non-deterministic.
Developers are encouraged to match the casing of file and directory names to the casing of:
Matching case ensures the deployments find their views regardless of the underlying file system.
Razor file compilation in ASP.NET Core
9/18/2018 • 2 minutes to read • Edit Online
By Rick Anderson
A Razor file is compiled at runtime, when the associated MVC view is invoked. Build-time Razor file publishing is
unsupported. Razor files can optionally be compiled at publish time and deployed with the app—using the
precompilation tool.
A Razor file is compiled at runtime, when the associated Razor Page or MVC view is invoked. Build-time Razor file
publishing is unsupported. Razor files can optionally be compiled at publish time and deployed with the app—
using the precompilation tool.
A Razor file is compiled at runtime, when the associated Razor Page or MVC view is invoked. Razor files are
compiled at both build and publish time using the Razor SDK.
Precompilation considerations
The following are side effects of precompiling Razor files:
A smaller published bundle
A faster startup time
You can't edit Razor files—the associated content is absent from the published bundle.
IMPORTANT
The precompilation tool will be removed in ASP.NET Core 3.0. We recommend migrating to Razor Sdk.
The Razor SDK is effective only when no precompilation-specific properties are set in the project file. For instance, setting the
.csproj file's MvcRazorCompileOnPublish property to true disables the Razor SDK.
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation"
Version="2.0.4"
PrivateAssets="All" />
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="1.1.0-*" />
</ItemGroup>
</Project>
Prepare the app for a framework-dependent deployment with the .NET Core CLI publish command. For example,
execute the following command at the project root:
Additional resources
Views in ASP.NET Core MVC
Introduction to Razor Pages in ASP.NET Core
Views in ASP.NET Core MVC
Introduction to Razor Pages in ASP.NET Core
Views in ASP.NET Core MVC
ASP.NET Core Razor SDK
Layout in ASP.NET Core
8/16/2018 • 5 minutes to read • Edit Online
By Steve Smith
Views frequently share visual and programmatic elements. In this article, you'll learn how to use common
layouts, share directives, and run common code before rendering views in your ASP.NET Core app.
What is a Layout
Most web apps have a common layout that provides the user with a consistent experience as they navigate
from page to page. The layout typically includes common user interface elements such as the app header,
navigation or menu elements, and footer.
Common HTML structures such as scripts and stylesheets are also frequently used by many pages within
an app. All of these shared elements may be defined in a layout file, which can then be referenced by any
view used within the app. Layouts reduce duplicate code in views, helping them follow the Don't Repeat
Yourself (DRY ) principle.
By convention, the default layout for an ASP.NET Core app is named _Layout.cshtml . The Visual Studio
ASP.NET Core MVC project template includes this layout file in the Views/Shared folder:
This layout defines a top level template for views in the app. Apps don't require a layout, and apps can
define more than one layout, with different views specifying different layouts.
An example _Layout.cshtml :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-
test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-
brand">WebApplication1</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
@await Html.PartialAsync("_LoginPartial")
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2016 - WebApplication1</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
Specifying a Layout
Razor views have a Layout property. Individual views specify a layout by setting this property:
@{
Layout = "_Layout";
}
The layout specified can use a full path (example: /Views/Shared/_Layout.cshtml ) or a partial name
(example: _Layout ). When a partial name is provided, the Razor view engine will search for the layout file
using its standard discovery process. The controller-associated folder is searched first, followed by the
Shared folder. This discovery process is identical to the one used to discover partial views.
By default, every layout must call RenderBody . Wherever the call to RenderBody is placed, the contents of
the view will be rendered.
Sections
A layout can optionally reference one or more sections, by calling RenderSection . Sections provide a way to
organize where certain page elements should be placed. Each call to RenderSection can specify whether
that section is required or optional. If a required section isn't found, an exception will be thrown. Individual
views specify the content to be rendered within a section using the @section Razor syntax. If a view defines
a section, it must be rendered (or an error will occur).
An example @section definition in a view:
@section Scripts {
<script type="text/javascript" src="/scripts/main.js"></script>
}
In the code above, validation scripts are added to the scripts section on a view that includes a form. Other
views in the same application might not require any additional scripts, and so wouldn't need to define a
scripts section.
Sections defined in a view are available only in its immediate layout page. They cannot be referenced from
partials, view components, or other parts of the view system.
Ignoring sections
By default, the body and all sections in a content page must all be rendered by the layout page. The Razor
view engine enforces this by tracking whether the body and each section have been rendered.
To instruct the view engine to ignore the body or sections, call the IgnoreBody and IgnoreSection methods.
The body and every section in a Razor page must be either rendered or ignored.
@addTagHelper
@removeTagHelper
@tagHelperPrefix
@using
@model
@inherits
@inject
The file doesn't support other Razor features, such as functions and section definitions.
A sample _ViewImports.cshtml file:
@using WebApplication1
@using WebApplication1.Models
@using WebApplication1.Models.AccountViewModels
@using WebApplication1.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The _ViewImports.cshtml file for an ASP.NET Core MVC app is typically placed in the Views folder. A
_ViewImports.cshtml file can be placed within any folder, in which case it will only be applied to views
within that folder and its subfolders. _ViewImports files are processed starting at the root level, and then for
each folder leading up to the location of the view itself, so settings specified at the root level may be
overridden at the folder level.
For example, if a root level _ViewImports.cshtml file specifies @model and @addTagHelper , and another
_ViewImports.cshtml file in the controller -associated folder of the view specifies a different @model and
adds another @addTagHelper , the view will have access to both tag helpers and will use the latter @model .
If multiple _ViewImports.cshtml files are run for a view, combined behavior of the directives included in the
ViewImports.cshtml files will be as follows:
@addTagHelper , @removeTagHelper : all run, in order
@tagHelperPrefix : the closest one to the view overrides any others
@model : the closest one to the view overrides any others
@inherits : the closest one to the view overrides any others
@using : all are included; duplicates are ignored
@inject : for each property, the closest one to the view overrides any others with the same property
name
@{
Layout = "_Layout";
}
The file above specifies that all views will use the _Layout.cshtml layout.
NOTE
Neither _ViewStart.cshtml nor _ViewImports.cshtml are typically placed in the /Views/Shared folder. The
app-level versions of these files should be placed directly in the /Views folder.
Tag Helpers in ASP.NET Core
8/16/2018 • 11 minutes to read • Edit Online
By Rick Anderson
Most built-in Tag Helpers target standard HTML elements and provide server-side attributes for the
element. For example, the <input> element used in many views in the Views/Account folder
contains the asp-for attribute. This attribute extracts the name of the specified model property into
the rendered HTML. Consider a Razor view with the following model:
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
<label asp-for="Movie.Title"></label>
<label for="Movie_Title">Title</label>
The asp-for attribute is made available by the For property in the LabelTagHelper. See Author Tag
Helpers for more information.
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
The @addTagHelper directive makes Tag Helpers available to the view. In this case, the view file is
Pages/_ViewImports.cshtml, which by default is inherited by all files in the Pages folder and sub-
folders; making Tag Helpers available. The code above uses the wildcard syntax ("*") to specify that
all Tag Helpers in the specified assembly (Microsoft.AspNetCore.Mvc.TagHelpers) will be available to
every view file in the Views directory or sub-directory. The first parameter after @addTagHelper
specifies the Tag Helpers to load (we are using "*" for all Tag Helpers), and the second parameter
"Microsoft.AspNetCore.Mvc.TagHelpers" specifies the assembly containing the Tag Helpers.
Microsoft.AspNetCore.Mvc.TagHelpers is the assembly for the built-in ASP.NET Core Tag Helpers.
To expose all of the Tag Helpers in this project (which creates an assembly named
AuthoringTagHelpers), you would use the following:
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
To add a Tag Helper to a view using an FQN, you first add the FQN (
AuthoringTagHelpers.TagHelpers.EmailTagHelper ), and then the assembly name
(AuthoringTagHelpers). Most developers prefer to use the "*" wildcard syntax. The wildcard syntax
allows you to insert the wildcard character "*" as the suffix in an FQN. For example, any of the
following directives will bring in the EmailTagHelper :
You must apply the Tag Helper opt-out character to the opening and closing tag. (The Visual Studio
editor automatically adds the opt-out character to the closing tag when you add one to the opening
tag). After you add the opt-out character, the element and Tag Helper attributes are no longer
displayed in a distinctive font.
Using @tagHelperPrefix to make Tag Helper usage explicit
The @tagHelperPrefix directive allows you to specify a tag prefix string to enable Tag Helper support
and to make Tag Helper usage explicit. For example, you could add the following markup to the
Views/_ViewImports.cshtml file:
@tagHelperPrefix th:
In the code image below, the Tag Helper prefix is set to th: , so only those elements using the prefix
th: support Tag Helpers (Tag Helper-enabled elements have a distinctive font). The <label> and
<input> elements have the Tag Helper prefix and are Tag Helper -enabled, while the <span> element
doesn't.
The same hierarchy rules that apply to @addTagHelper also apply to @tagHelperPrefix .
Not only do you get HTML help, but the icon (the "@" symbol with "<>" under it).
identifies the element as targeted by Tag Helpers. Pure HTML elements (such as the fieldset )
display the "<>" icon.
A pure HTML <label> tag displays the HTML tag (with the default Visual Studio color theme) in a
brown font, the attributes in red, and the attribute values in blue.
After you enter <label , IntelliSense lists the available HTML/CSS attributes and the Tag Helper-
targeted attributes:
IntelliSense statement completion allows you to enter the tab key to complete the statement with the
selected value:
As soon as a Tag Helper attribute is entered, the tag and attribute fonts change. Using the default
Visual Studio "Blue" or "Light" color theme, the font is bold purple. If you're using the "Dark" theme
the font is bold teal. The images in this document were taken using the default theme.
You can enter the Visual Studio CompleteWord shortcut (Ctrl +spacebar is the default inside the
double quotes (""), and you are now in C#, just like you would be in a C# class. IntelliSense displays
all the methods and properties on the page model. The methods and properties are available
because the property type is ModelExpression . In the image below, I'm editing the Register view, so
the RegisterViewModel is available.
IntelliSense lists the properties and methods available to the model on the page. The rich
IntelliSense environment helps you select the CSS class:
The at ( @ ) symbol tells Razor this is the start of code. The next two parameters ("FirstName" and
"First Name:") are strings, so IntelliSense can't help. The last argument:
new {@class="caption"}
Is an anonymous object used to represent attributes. Because class is a reserved keyword in C#, you
use the @ symbol to force C# to interpret "@class=" as a symbol (property name). To a front-end
designer (someone familiar with HTML/CSS/JavaScript and other client technologies but not
familiar with C# and Razor), most of the line is foreign. The entire line must be authored with no
help from IntelliSense.
Using the LabelTagHelper , the same markup can be written as:
With the Tag Helper version, as soon as you enter <l in the Visual Studio editor, IntelliSense
displays matching elements:
IntelliSense helps you write the entire line. The LabelTagHelper also defaults to setting the content of
the asp-for attribute value ("FirstName") to "First Name"; It converts camel-cased properties to a
sentence composed of the property name with a space where each new upper-case letter occurs. In
the following markup:
generates:
The camel-cased to sentence-cased content isn't used if you add content to the <label> . For
example:
generates:
The following code image shows the Form portion of the Views/Account/Register.cshtml Razor view
generated from the legacy ASP.NET 4.5.x MVC template included with Visual Studio 2015.
The Visual Studio editor displays C# code with a grey background. For example, the
AntiForgeryToken HTML Helper:
@Html.AntiForgeryToken()
is displayed with a grey background. Most of the markup in the Register view is C#. Compare that to
the equivalent approach using Tag Helpers:
The markup is much cleaner and easier to read, edit, and maintain than the HTML Helpers approach.
The C# code is reduced to the minimum that the server needs to know about. The Visual Studio
editor displays markup targeted by a Tag Helper in a distinctive font.
Consider the Email group:
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
Each of the "asp-" attributes has a value of "Email", but "Email" isn't a string. In this context, "Email" is
the C# model expression property for the RegisterViewModel .
The Visual Studio editor helps you write all of the markup in the Tag Helper approach of the register
form, while Visual Studio provides no help for most of the code in the HTML Helpers approach.
IntelliSense support for Tag Helpers goes into detail on working with Tag Helpers in the Visual
Studio editor.
By Rick Anderson
View or download sample code (how to download)
<email>Support</email>
The server will use our email tag helper to convert that markup into the following:
<a href="mailto:[email protected]">[email protected]</a>
That is, an anchor tag that makes this an email link. You might want to do this if you are writing a blog engine
and need it to send email for marketing, support, and other contacts, all to the same domain.
1. Add the following EmailTagHelper class to the TagHelpers folder.
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;
namespace AuthoringTagHelpers.TagHelpers
{
public class EmailTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
}
}
}
Notes:
Tag helpers use a naming convention that targets elements of the root class name (minus the
TagHelper portion of the class name). In this example, the root name of EmailTagHelper is email,
so the <email> tag will be targeted. This naming convention should work for most tag helpers,
later on I'll show how to override it.
The EmailTagHelper class derives from TagHelper . The TagHelper class provides methods and
properties for writing Tag Helpers.
The overridden Process method controls what the tag helper does when executed. The TagHelper
class also provides an asynchronous version ( ProcessAsync ) with the same parameters.
The context parameter to Process (and ProcessAsync ) contains information associated with the
execution of the current HTML tag.
The output parameter to Process (and ProcessAsync ) contains a stateful HTML element
representative of the original source used to generate an HTML tag and content.
Our class name has a suffix of TagHelper, which is not required, but it's considered a best practice
convention. You could declare the class as:
2. To make the EmailTagHelper class available to all our Razor views, add the addTagHelper directive to the
Views/_ViewImports.cshtml file:
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
The code above uses the wildcard syntax to specify all the tag helpers in our assembly will be available.
The first string after @addTagHelper specifies the tag helper to load (Use "*" for all tag helpers), and the
second string "AuthoringTagHelpers" specifies the assembly the tag helper is in. Also, note that the
second line brings in the ASP.NET Core MVC tag helpers using the wildcard syntax (those helpers are
discussed in Introduction to Tag Helpers.) It's the @addTagHelper directive that makes the tag helper
available to the Razor view. Alternatively, you can provide the fully qualified name (FQN ) of a tag helper
as shown below:
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers
To add a tag helper to a view using a FQN, you first add the FQN (
AuthoringTagHelpers.TagHelpers.EmailTagHelper ), and then the assembly name ( AuthoringTagHelpers). Most
developers will prefer to use the wildcard syntax. Introduction to Tag Helpers goes into detail on tag helper
adding, removing, hierarchy, and wildcard syntax.
3. Update the markup in the Views/Home/Contact.cshtml file with these changes:
@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>
4. Run the app and use your favorite browser to view the HTML source so you can verify that the email tags
are replaced with anchor markup (For example, <a>Support</a> ). Support and Marketing are rendered as
a links, but they don't have an href attribute to make them functional. We'll fix that in the next section.
Notes:
Pascal-cased class and property names for tag helpers are translated into their lower kebab case.
Therefore, to use the MailTo attribute, you'll use <email mail-to="value"/> equivalent.
The last line sets the completed content for our minimally functional tag helper.
The highlighted line shows the syntax for adding attributes:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
That approach works for the attribute "href" as long as it doesn't currently exist in the attributes collection. You
can also use the output.Attributes.Add method to add a tag helper attribute to the end of the collection of tag
attributes.
1. Update the markup in the Views/Home/Contact.cshtml file with these changes:
@{
ViewData["Title"] = "Contact Copy";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<address>
One Microsoft Way Copy Version <br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong><email mail-to="Support"></email><br />
<strong>Marketing:</strong><email mail-to="Marketing"></email>
</address>
2. Run the app and verify that it generates the correct links.
NOTE
If you were to write the email tag self-closing ( <email mail-to="Rick" /> ), the final output would also be self-
closing. To enable the ability to write the tag with only a start tag ( <email mail-to="Rick"> ) you must decorate
the class with the following:
With a self-closing email tag helper, the output would be <a href="mailto:[email protected]" /> . Self-
closing anchor tags are not valid HTML, so you wouldn't want to create one, but you might want to create
a tag helper that's self-closing. Tag helpers set the type of the TagMode property after reading a tag.
ProcessAsync
In this section, we'll write an asynchronous email helper.
1. Replace the EmailTagHelper class with the following code:
public class EmailTagHelper : TagHelper
{
private const string EmailDomain = "contoso.com";
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // Replaces <email> with <a> tag
var content = await output.GetChildContentAsync();
var target = content.GetContent() + "@" + EmailDomain;
output.Attributes.SetAttribute("href", "mailto:" + target);
output.Content.SetContent(target);
}
}
Notes:
This version uses the asynchronous ProcessAsync method. The asynchronous
GetChildContentAsync returns a Task containing the TagHelperContent .
@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>
3. Run the app and verify that it generates valid email links.
RemoveAll, PreContent.SetHtmlContent and PostContent.SetHtmlContent
1. Add the following BoldTagHelper class to the TagHelpers folder.
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}
}
Notes:
The [HtmlTargetElement] attribute passes an attribute parameter that specifies that any HTML
element that contains an HTML attribute named "bold" will match, and the Process override
method in the class will run. In our sample, the Process method removes the "bold" attribute and
surrounds the containing markup with <strong></strong> .
Because you don't want to replace the existing tag content, you must write the opening <strong>
tag with the PreContent.SetHtmlContent method and the closing </strong> tag with the
PostContent.SetHtmlContent method.
2. Modify the About.cshtml view to contain a bold attribute value. The completed code is shown below.
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
3. Run the app. You can use your favorite browser to inspect the source and verify the markup.
The [HtmlTargetElement] attribute above only targets HTML markup that provides an attribute name of
"bold". The <bold> element wasn't modified by the tag helper.
4. Comment out the [HtmlTargetElement] attribute line and it will default to targeting <bold> tags, that is,
HTML markup of the form <bold> . Remember, the default naming convention will match the class name
BoldTagHelper to <bold> tags.
5. Run the app and verify that the <bold> tag is processed by the tag helper.
Decorating a class with multiple [HtmlTargetElement] attributes results in a logical-OR of the targets. For
example, using the code below, a bold tag or a bold attribute will match.
[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}
When multiple attributes are added to the same statement, the runtime treats them as a logical-AND. For
example, in the code below, an HTML element must be named "bold" with an attribute named "bold" (
<bold bold /> ) to match.
You can also use the [HtmlTargetElement] to change the name of the targeted element. For example if you
wanted the BoldTagHelper to target <MyBold> tags, you would use the following attribute:
[HtmlTargetElement("MyBold")]
using System;
namespace AuthoringTagHelpers.Models
{
public class WebsiteContext
{
public Version Version { get; set; }
public int CopyrightYear { get; set; }
public bool Approved { get; set; }
public int TagsToShow { get; set; }
}
}
using System;
using AuthoringTagHelpers.Models;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AuthoringTagHelpers.TagHelpers
{
public class WebsiteInformationTagHelper : TagHelper
{
public WebsiteContext Info { get; set; }
Notes:
As mentioned previously, tag helpers translates Pascal-cased C# class names and properties for
tag helpers into lower kebab case. Therefore, to use the WebsiteInformationTagHelper in Razor,
you'll write <website-information /> .
You are not explicitly identifying the target element with the [HtmlTargetElement] attribute, so the
default of website-information will be targeted. If you applied the following attribute (note it's not
kebab case but matches the class name):
[HtmlTargetElement("WebsiteInformation")]
The lower kebab case tag <website-information /> wouldn't match. If you want use the
[HtmlTargetElement] attribute, you would use kebab case as shown below:
[HtmlTargetElement("Website-Information")]
Elements that are self-closing have no content. For this example, the Razor markup will use a self-
closing tag, but the tag helper will be creating a section element (which isn't self-closing and you
are writing content inside the section element). Therefore, you need to set TagMode to
StartTagAndEndTag to write output. Alternatively, you can comment out the line setting TagMode
and write markup with a closing tag. (Example markup is provided later in this tutorial.)
The $ (dollar sign) in the following line uses an interpolated string:
$@"<ul><li><strong>Version:</strong> {Info.Version}</li>
4. Add the following markup to the About.cshtml view. The highlighted markup displays the web site
information.
@using AuthoringTagHelpers.Models
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
NOTE
In the Razor markup shown below:
Razor knows the info attribute is a class, not a string, and you want to write C# code. Any non-string tag helper
attribute should be written without the @ character.
5. Run the app, and navigate to the About view to see the web site information.
NOTE
You can use the following markup with a closing tag and remove the line with TagMode.StartTagAndEndTag in
the tag helper:
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace AuthoringTagHelpers.TagHelpers
{
[HtmlTargetElement(Attributes = nameof(Condition))]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }
2. Replace the contents of the Views/Home/Index.cshtml file with the following markup:
@using AuthoringTagHelpers.Models
@model WebsiteContext
@{
ViewData["Title"] = "Home Page";
}
<div>
<h3>Information about our website (outdated):</h3>
<Website-InforMation info="Model" />
<div condition="Model.Approved">
<p>
This website has <strong surround="em">@Model.Approved</strong> been approved yet.
Visit www.contoso.com for more information.
</p>
</div>
</div>
3. Replace the Index method in the Home controller with the following code:
public IActionResult Index(bool approved = false)
{
return View(new WebsiteContext
{
Approved = approved,
CopyrightYear = 2015,
Version = new Version(1, 3, 3, 7),
TagsToShow = 20
});
}
4. Run the app and browse to the home page. The markup in the conditional div won't be rendered.
Append the query string ?approved=true to the URL (for example,
http://localhost:1235/Home/Index?approved=true ). approved is set to true and the conditional markup will
be displayed.
NOTE
Use the nameof operator to specify the attribute to target rather than specifying a string as you did with the bold tag
helper:
[HtmlTargetElement(Attributes = nameof(Condition))]
// [HtmlTargetElement(Attributes = "condition")]
public class ConditionTagHelper : TagHelper
{
public bool Condition { get; set; }
The nameof operator will protect the code should it ever be refactored (we might want to change the name to
RedCondition ).
NOTE
The AutoLinkerHttpTagHelper class targets p elements and uses Regex to create the anchor.
@{
ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<address>
One Microsoft Way<br />
Redmond, WA 98052<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
<address>
<strong>Support:</strong><email>Support</email><br />
<strong>Marketing:</strong><email>Marketing</email>
</address>
3. Run the app and verify that the tag helper renders the anchor correctly.
4. Update the AutoLinker class to include the AutoLinkerWwwTagHelper which will convert www text to an
anchor tag that also contains the original www text. The updated code is highlighted below:
[HtmlTargetElement("p")]
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}
[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent.GetContent(),
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version
}
}
}
5. Run the app. Notice the www text is rendered as a link but the HTTP text isn't. If you put a break point in
both classes, you can see that the HTTP tag helper class runs first. The problem is that the tag helper
output is cached, and when the WWW tag helper is run, it overwrites the cached output from the HTTP
tag helper. Later in the tutorial we'll see how to control the order that tag helpers run in. We'll fix the code
with the following:
public class AutoLinkerHttpTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}
[HtmlTargetElement("p")]
public class AutoLinkerWwwTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = output.Content.IsModified ? output.Content.GetContent() :
(await output.GetChildContentAsync()).GetContent();
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(www\.)(\S+)\b",
"<a target=\"_blank\" href=\"http://$0\">$0</a>")); // www version
}
}
NOTE
In the first edition of the auto-linking tag helpers, you got the content of the target with the following code:
That is, you call GetChildContentAsync using the TagHelperOutput passed into the ProcessAsync method.
As mentioned previously, because the output is cached, the last tag helper to run wins. You fixed that problem with
the following code:
The code above checks to see if the content has been modified, and if it has, it gets the content from the output
buffer.
6. Run the app and verify that the two links work as expected. While it might appear our auto linker tag
helper is correct and complete, it has a subtle problem. If the WWW tag helper runs first, the www links
won't be correct. Update the code by adding the Order overload to control the order that the tag runs in.
The Order property determines the execution order relative to other tag helpers targeting the same
element. The default order value is zero and instances with lower values are executed first.
public class AutoLinkerHttpTagHelper : TagHelper
{
// This filter must run before the AutoLinkerWwwTagHelper as it searches and replaces http and
// the AutoLinkerWwwTagHelper adds http to the markup.
public override int Order
{
get { return int.MinValue; }
}
The above code will guarantee that the HTTP tag helper runs before the WWW tag helper. Change Order
to MaxValue and verify that the markup generated for the WWW tag is incorrect.
// Find Urls in the content and replace them with their anchor tag equivalent.
output.Content.SetHtmlContent(Regex.Replace(
childContent,
@"\b(?:https?://)(\S+)\b",
"<a target=\"_blank\" href=\"$0\">$0</a>")); // http link version}
}
}
Multiple calls to GetChildContentAsync returns the same value and doesn't re-execute the TagHelper body
unless you pass in a false parameter indicating not to use the cached result.
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Tag Helper Components in ASP.NET Core
9/18/2018 • 5 minutes to read • Edit Online
Use cases
Two common use cases of Tag Helper Components include:
1. Injecting a <link> into the <head> .
2. Injecting a <script> into the <body> .
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace RazorPagesSample.TagHelpers
{
public class AddressStyleTagHelperComponent : TagHelperComponent
{
private readonly string _style =
@"<link rel=""stylesheet"" href=""/css/address.css"" />";
return Task.CompletedTask;
}
}
}
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace RazorPagesSample.TagHelpers
{
public class AddressScriptTagHelperComponent : TagHelperComponent
{
public override int Order => 2;
A separate HTML file is used to store the <script> element. The HTML file makes the code cleaner and more
maintainable. The preceding code reads the contents of TagHelpers/Templates/AddressToolTipScript.html and
appends it with the Tag Helper output. The AddressToolTipScript.html file includes the following markup:
<script>
$("address[printable]").hover(function() {
$(this).attr({
"data-toggle": "tooltip",
"data-placement": "right",
"title": "Home of Microsoft!"
});
});
</script>
The preceding code binds a Bootstrap tooltip widget to any <address> element that includes a printable
attribute. The effect is visible when a mouse pointer hovers over the element.
Register a Component
A Tag Helper Component must be added to the app's Tag Helper Components collection. There are three ways to
add to the collection:
1. Registration via services container
2. Registration via Razor file
3. Registration via Page Model or controller
Registration via services container
If the Tag Helper Component class isn't managed with ITagHelperComponentManager, it must be registered with
the dependency injection (DI) system. The following Startup.ConfigureServices code registers the
AddressStyleTagHelperComponent and AddressScriptTagHelperComponent classes with a transient lifetime:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddTransient<ITagHelperComponent,
AddressScriptTagHelperComponent>();
services.AddTransient<ITagHelperComponent,
AddressStyleTagHelperComponent>();
}
@using RazorPagesSample.TagHelpers;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager manager;
@{
string markup;
if (Model.IsWeekend)
{
markup = "<em class='text-warning'>Office closed today!</em>";
}
else
{
markup = "<em class='text-info'>Office open today!</em>";
}
AddressTagHelperComponent is modified to accommodate a constructor that accepts the markup and order
parameters:
if (IsWeekend)
{
markup = "<em class='text-warning'>Office closed today!</em>";
}
else
{
markup = "<em class='text-info'>Office open today!</em>";
}
_tagHelperComponentManager.Components.Add(
new AddressTagHelperComponent(markup, 1));
}
}
Create a Component
To create a custom Tag Helper Component:
Create a public class deriving from TagHelperComponentTagHelper.
Apply an [HtmlTargetElement] attribute to the class. Specify the name of the target HTML element.
Optional: Apply an [EditorBrowsable(EditorBrowsableState.Never)] attribute to the class to suppress the type's
display in IntelliSense.
The following code creates a custom Tag Helper Component that targets the <address> HTML element:
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;
namespace RazorPagesSample.TagHelpers
{
[HtmlTargetElement("address")]
[EditorBrowsable(EditorBrowsableState.Never)]
public class AddressTagHelperComponentTagHelper : TagHelperComponentTagHelper
{
public AddressTagHelperComponentTagHelper(
ITagHelperComponentManager componentManager,
ILoggerFactory loggerFactory) : base(componentManager, loggerFactory)
{
}
}
}
Use the custom address Tag Helper Component to inject HTML markup as follows:
The preceding ProcessAsync method injects the HTML provided to SetHtmlContent into the matching <address>
element. The injection occurs when:
The execution context's TagName property value equals address .
The corresponding <address> element has a printable attribute.
For example, the if statement evaluates to true when processing the following <address> element:
<address printable>
One Microsoft Way<br />
Redmond, WA 98052-6399<br />
<abbr title="Phone">P:</abbr>
425.555.0100
</address>
Additional resources
Dependency injection in ASP.NET Core
Dependency injection into views in ASP.NET Core
ASP.NET Core built-in Tag Helpers
ASP.NET Core built-in Tag Helpers
9/18/2018 • 2 minutes to read • Edit Online
By Peter Kellner
ASP.NET Core includes many built-in Tag Helpers to boost your productivity. This section provides an overview of
the built-in Tag Helpers.
NOTE
There are built-in Tag Helpers which aren't discussed, since they're used internally by the Razor view engine. This includes a
Tag Helper for the ~ character, which expands to the root path of the website.
Additional resources
Tag Helpers in ASP.NET Core
Tag Helper Components in ASP.NET Core
Anchor Tag Helper in ASP.NET Core
6/21/2018 • 6 minutes to read • Edit Online
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
[Route("Speaker/{id:int}")]
public IActionResult Detail(int id) =>
View(Speakers.FirstOrDefault(a => a.SpeakerId == id));
[Route("/Speaker/Evaluations",
Name = "speakerevals")]
public IActionResult Evaluations() => View();
[Route("/Speaker/EvaluationsCurrent",
Name = "speakerevalscurrent")]
public IActionResult Evaluations(
int speakerId,
bool currentYear) => View();
asp-controller
The asp-controller attribute assigns the controller used for generating the URL. The following markup lists all
speakers:
<a asp-controller="Speaker"
asp-action="Index">All Speakers</a>
If the asp-controller attribute is specified and asp-action isn't, the default asp-action value is the controller
action associated with the currently executing view. If asp-action is omitted from the preceding markup, and
the Anchor Tag Helper is used in HomeController's Index view (/Home), the generated HTML is:
asp-action
The asp-action attribute value represents the controller action name included in the generated href attribute.
The following markup sets the generated href attribute value to the speaker evaluations page:
<a asp-controller="Speaker"
asp-action="Evaluations">Speaker Evaluations</a>
If no asp-controller attribute is specified, the default controller calling the view executing the current view is
used.
If the asp-action attribute value is Index , then no action is appended to the URL, leading to the invocation of
the default Index action. The action specified (or defaulted), must exist in the controller referenced in
asp-controller .
asp-route-{value}
The asp-route-{value} attribute enables a wildcard route prefix. Any value occupying the {value} placeholder is
interpreted as a potential route parameter. If a default route isn't found, this route prefix is appended to the
generated href attribute as a request parameter and value. Otherwise, it's substituted in the route template.
Consider the following controller action:
return View(speaker);
}
The MVC view uses the model, provided by the action, as follows:
@model Speaker
<!DOCTYPE html>
<html>
<body>
<a asp-controller="Speaker"
asp-action="Detail"
asp-route-id="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
</body>
</html>
The default route's {id?} placeholder was matched. The generated HTML:
Assume the route prefix isn't part of the matching routing template, as with the following MVC view:
@model Speaker
<!DOCTYPE html>
<html>
<body>
<a asp-controller="Speaker"
asp-action="Detail"
asp-route-speakerid="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
<body>
</html>
The following HTML is generated because speakerid wasn't found in the matching route:
If either asp-controller or asp-action aren't specified, then the same default processing is followed as is in the
asp-route attribute.
asp-route
The asp-route attribute is used for creating a URL linking directly to a named route. Using routing attributes, a
route can be named as shown in the SpeakerController and used in its Evaluations action:
[Route("/Speaker/Evaluations",
Name = "speakerevals")]
public IActionResult Evaluations() => View();
In the following markup, the asp-route attribute references the named route:
The Anchor Tag Helper generates a route directly to that controller action using the URL /Speaker/Evaluations.
The generated HTML:
If asp-controller or asp-action is specified in addition to asp-route , the route generated may not be what
you expect. To avoid a route conflict, asp-route shouldn't be used with the asp-controller and asp-action
attributes.
asp-all-route-data
The asp-all-route-data attribute supports the creation of a dictionary of key-value pairs. The key is the
parameter name, and the value is the parameter value.
In the following example, a dictionary is initialized and passed to a Razor view. Alternatively, the data could be
passed in with your model.
@{
var parms = new Dictionary<string, string>
{
{ "speakerId", "11" },
{ "currentYear", "true" }
};
}
<a asp-route="speakerevalscurrent"
asp-all-route-data="parms">Speaker Evaluations</a>
The asp-all-route-data dictionary is flattened to produce a querystring meeting the requirements of the
overloaded Evaluations action:
[Route("/Speaker/EvaluationsCurrent",
Name = "speakerevalscurrent")]
public IActionResult Evaluations(
int speakerId,
bool currentYear) => View();
If any keys in the dictionary match route parameters, those values are substituted in the route as appropriate.
The other non-matching values are generated as request parameters.
asp-fragment
The asp-fragment attribute defines a URL fragment to append to the URL. The Anchor Tag Helper adds the
hash character (#). Consider the following markup:
<a asp-controller="Speaker"
asp-action="Evaluations"
asp-fragment="SpeakerEvaluations">Speaker Evaluations</a>
Hash tags are useful when building client-side apps. They can be used for easy marking and searching in
JavaScript, for example.
asp-area
The asp-area attribute sets the area name used to set the appropriate route. The following example depicts how
the area attribute causes a remapping of routes. Setting asp-area to "Blogs" prefixes the directory Areas/Blogs
to the routes of the associated controllers and views for this anchor tag.
<Project name>
wwwroot
Areas
Blogs
Controllers
HomeController.cs
Views
Home
AboutBlog.cshtml
Index.cshtml
_ViewStart.cshtml
Controllers
Given the preceding directory hierarchy, the markup to reference the AboutBlog.cshtml file is:
<a asp-area="Blogs"
asp-controller="Home"
asp-action="AboutBlog">About Blog</a>
app.UseMvc(routes =>
{
// need route and attribute on controller: [Area("Blogs")]
routes.MapRoute(name: "areaRoute",
template: "{area:exists}/{controller=Home}/{action=Index}");
asp-protocol
The asp-protocol attribute is for specifying a protocol (such as https ) in your URL. For example:
<a asp-protocol="https"
asp-controller="Home"
asp-action="About">About</a>
<a href="https://localhost/Home/About">About</a>
The host name in the example is localhost, but the Anchor Tag Helper uses the website's public domain when
generating the URL.
asp-host
The asp-host attribute is for specifying a host name in your URL. For example:
<a asp-protocol="https"
asp-host="microsoft.com"
asp-controller="Home"
asp-action="About">About</a>
<a href="https://microsoft.com/Home/About">About</a>
asp-page
The asp-page attribute is used with Razor Pages. Use it to set an anchor tag's href attribute value to a specific
page. Prefixing the page name with a forward slash ("/") creates the URL.
The following sample points to the attendee Razor Page:
The asp-page attribute is mutually exclusive with the asp-route , asp-controller , and asp-action attributes.
However, asp-page can be used with asp-route-{value} to control routing, as the following markup
demonstrates:
<a asp-page="/Attendee"
asp-route-attendeeid="10">View Attendee</a>
asp-page-handler
The asp-page-handler attribute is used with Razor Pages. It's intended for linking to specific page handlers.
Consider the following page handler:
The page model's associated markup links to the OnGetProfile page handler. Note that the On<Verb> prefix of
the page handler method name is omitted in the asp-page-handler attribute value. If this were an asynchronous
method, the Async suffix would be omitted too.
<a asp-page="/Attendee"
asp-page-handler="Profile"
asp-route-attendeeid="12">Attendee Profile</a>
Additional resources
Areas
Intro to Razor Pages
Cache Tag Helper in ASP.NET Core MVC
9/24/2018 • 4 minutes to read • Edit Online
By Peter Kellner
The Cache Tag Helper provides the ability to dramatically improve the performance of your ASP.NET Core app
by caching its content to the internal ASP.NET Core cache provider.
The Razor View Engine sets the default expires-after to twenty minutes.
The following Razor markup caches the date/time:
<cache>@DateTime.Now</cache>
The first request to the page that contains CacheTagHelper will display the current date/time. Additional requests
will show the cached value until the cache expires (default 20 minutes) or is evicted by memory pressure.
You can set the cache duration with the following attributes:
"false"
Determines whether the content enclosed by the Cache Tag Helper is cached. The default is true . If set to false
this Cache Tag Helper will have no caching effect on the rendered output.
Example:
<cache enabled="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
expires-on
ATTRIBUTE TYPE EXAMPLE VALUE
Sets an absolute expiration date. The following example will cache the contents of the Cache Tag Helper until 5:02
PM on January 29, 2025.
Example:
<cache expires-on="@new DateTime(2025,1,29,17,02,0)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
expires-after
ATTRIBUTE TYPE EXAMPLE VALUE
TimeSpan "@TimeSpan.FromSeconds(120)"
Sets the length of time from the first request time to cache the contents.
Example:
<cache expires-after="@TimeSpan.FromSeconds(120)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
expires-sliding
ATTRIBUTE TYPE EXAMPLE VALUE
TimeSpan "@TimeSpan.FromSeconds(60)"
Sets the time that a cache entry should be evicted if it has not been accessed.
Example:
<cache expires-sliding="@TimeSpan.FromSeconds(60)">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-header
ATTRIBUTE TYPE EXAMPLE VALUES
String "User-Agent"
"User-Agent,content-encoding"
Accepts a single header value or a comma-separated list of header values that trigger a cache refresh when they
change. The following example monitors the header value User-Agent . The example will cache the content for
every different User-Agent presented to the web server.
Example:
<cache vary-by-header="User-Agent">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-query
ATTRIBUTE TYPE EXAMPLE VALUES
String "Make"
"Make,Model"
Accepts a single header value or a comma-separated list of header values that trigger a cache refresh when the
header value changes. The following example looks at the values of Make and Model .
Example:
<cache vary-by-query="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-route
ATTRIBUTE TYPE EXAMPLE VALUES
String "Make"
"Make,Model"
Accepts a single header value or a comma-separated list of header values that trigger a cache refresh when the
route data parameter value(s) change. Example:
Startup.cs
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{Make?}/{Model?}");
Index.cshtml
<cache vary-by-route="Make,Model">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-cookie
ATTRIBUTE TYPE EXAMPLE VALUES
String ".AspNetCore.Identity.Application"
".AspNetCore.Identity.Application,HairColor"
Accepts a single header value or a comma-separated list of header values that trigger a cache refresh when the
header values(s) change. The following example looks at the cookie associated with ASP.NET Core Identity.
When a user is authenticated the request cookie to be set which triggers a cache refresh.
Example:
<cache vary-by-cookie=".AspNetCore.Identity.Application">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
vary-by-user
ATTRIBUTE TYPE EXAMPLE VALUES
Boolean "true"
"false" (default)
Specifies whether or not the cache should reset when the logged-in user (or Context Principal) changes. The
current user is also known as the Request Context Principal and can be viewed in a Razor view by referencing
@User.Identity.Name .
<cache vary-by-user="true">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
Using this attribute maintains the contents in cache through a log-in and log-out cycle. When using
vary-by-user="true" , a log-in and log-out action invalidates the cache for the authenticated user. The cache is
invalidated because a new unique cookie value is generated on login. Cache is maintained for the anonymous
state when no cookie is present or has expired. This means if no user is logged in, the cache will be maintained.
vary-by
ATTRIBUTE TYPE EXAMPLE VALUES
String "@Model"
Allows for customization of what data gets cached. When the object referenced by the attribute's string value
changes, the content of the Cache Tag Helper is updated. Often a string-concatenation of model values are
assigned to this attribute. Effectively, that means an update to any of the concatenated values invalidates the
cache.
The following example assumes the controller method rendering the view sums the integer value of the two route
parameters, myParam1 and myParam2 , and returns that as the single model property. When this sum changes, the
content of the Cache Tag Helper is rendered and cached again.
Example:
Action:
public IActionResult Index(string myParam1,string myParam2,string myParam3)
{
int num1;
int num2;
int.TryParse(myParam1, out num1);
int.TryParse(myParam2, out num2);
return View(viewName, num1 + num2);
}
Index.cshtml
<cache vary-by="@Model"">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
priority
ATTRIBUTE TYPE EXAMPLE VALUES
CacheItemPriority "High"
"Low"
"NeverRemove"
"Normal"
Provides cache eviction guidance to the built-in cache provider. The web server will evict Low cache entries first
when it's under memory pressure.
Example:
<cache priority="High">
Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
The priority attribute doesn't guarantee a specific level of cache retention. CacheItemPriority is only a
suggestion. Setting this attribute to NeverRemove doesn't guarantee that the cache will always be retained. See
Additional Resources for more information.
The Cache Tag Helper is dependent on the memory cache service. The Cache Tag Helper adds the service if it has
not been added.
Additional resources
Cache in-memory
Introduction to Identity
Distributed Cache Tag Helper in ASP.NET Core
9/24/2018 • 2 minutes to read • Edit Online
By Peter Kellner
The Distributed Cache Tag Helper provides the ability to dramatically improve the performance of your ASP.NET
Core app by caching its content to a distributed cache source.
The Distributed Cache Tag Helper inherits from the same base class as the Cache Tag Helper. All attributes
associated with the Cache Tag Helper will also work on the Distributed Tag Helper.
The Distributed Cache Tag Helper follows the Explicit Dependencies Principle known as Constructor
Injection. Specifically, the IDistributedCache interface container is passed into the Distributed Cache Tag
Helper's constructor. If no specific concrete implementation of IDistributedCache has been created in
ConfigureServices , usually found in startup.cs, then the Distributed Cache Tag Helper will use the same in-
memory provider for storing cached data as the basic Cache Tag Helper.
name (required)
ATTRIBUTE TYPE EXAMPLE VALUE
string "my-distributed-cache-unique-key-101"
The required name attribute is used as a key to that cache stored for each instance of a Distributed Cache Tag
Helper. Unlike the basic Cache Tag Helper that assigns a key to each Cache Tag Helper instance based on the
Razor page name and location of the Tag Helper in the razor page, the Distributed Cache Tag Helper only bases
its key on the attribute name
Usage Example:
<distributed-cache name="my-distributed-cache-unique-key-101">
Time Inside Cache Tag Helper: @DateTime.Now
</distributed-cache>
<environment names="Staging,Production">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>
<environment include="Staging,Production">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>
<environment exclude="Development">
<strong>HostingEnvironment.EnvironmentName is Staging or Production</strong>
</environment>
Additional resources
Use multiple environments in ASP.NET Core
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Image Tag Helper in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
By Peter Kellner
The Image Tag Helper enhances the img ( <img> ) tag. It requires a src tag as well as the boolean attribute
asp-append-version .
If the image source ( src ) is a static file on the host web server, a unique cache busting string is appended as a
query parameter to the image source. This ensures that if the file on the host web server changes, a unique request
URL is generated that includes the updated request parameter. The cache busting string is a unique value
representing the hash of the static image file.
If the image source ( src ) isn't a static file (for example a remote URL or the file doesn't exist on the server), the
<img> tag's src attribute is generated with no cache busting query string parameter.
<img src="~/images/asplogo.png"
asp-append-version="true" />
If the static file exists in the directory ..wwwroot/images/asplogo.png the generated html is similar to the following
(the hash will be different):
<img
src="/images/asplogo.png?v=Kl_dqr9NVtnMdsM2MUg4qthUnWZm5T1fCEimBPWDNgM"/>
The value assigned to the parameter v is the hash value of the file on disk. If the web server is unable to obtain
read access to the static file referenced, no v parameters is added to the src attribute.
src
To activate the Image Tag Helper, the src attribute is required on the <img> element.
NOTE
The Image Tag Helper uses the Cache provider on the local web server to store the calculated Sha512 of a given file. If the
file is requested again the Sha512 doesn't need to be recalculated. The Cache is invalidated by a file watcher that's attached
to the file when the file's Sha512 is calculated.
Additional resources
Cache in-memory in ASP.NET Core
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Partial Tag Helper in ASP.NET Core
7/25/2018 • 3 minutes to read • Edit Online
By Scott Addie
View or download sample code (how to download)
Overview
The Partial Tag Helper is used for rendering a partial view in Razor Pages and MVC apps. Consider that it:
Requires ASP.NET Core 2.1 or later.
Is an alternative to HTML Helper syntax.
Renders the partial view asynchronously.
The HTML Helper options for rendering a partial view include:
@await Html.PartialAsync
@await Html.RenderPartialAsync
@Html.Partial
@Html.RenderPartial
The Product model is used in samples throughout this document:
namespace TagHelpersBuiltIn.Models
{
public class Product
{
public int Number { get; set; }
name
The name attribute is required. It indicates the name or the path of the partial view to be rendered. When a partial
view name is provided, the view discovery process is initiated. That process is bypassed when an explicit path is
provided. For all acceptable name values, see Partial view discovery.
The following markup uses an explicit path, indicating that _ProductPartial.cshtml is to be loaded from the Shared
folder. Using the for attribute, a model is passed to the partial view for binding.
<partial name="Shared/_ProductPartial.cshtml"
for="Product" />
for
The for attribute assigns a ModelExpression to be evaluated against the current model. A ModelExpression infers
the @Model. syntax. For example, for="Product" can be used instead of for="@Model.Product" . This default
inference behavior is overridden by using the @ symbol to define an inline expression. The for attribute can't be
used with the model attribute.
The following markup loads _ProductPartial.cshtml:
<partial name="_ProductPartial"
for="Product" />
The partial view is bound to the associated page model's Product property:
using Microsoft.AspNetCore.Mvc.RazorPages;
using TagHelpersBuiltIn.Models;
namespace TagHelpersBuiltIn.Pages
{
public class ProductModel : PageModel
{
public Product Product { get; set; }
model
The model attribute assigns a model instance to pass to the partial view. The model attribute can't be used with
the for attribute.
In the following markup, a new Product object is instantiated and passed to the model attribute for binding:
<partial name="_ProductPartial"
model='new Product { Number = 1, Name = "Test product", Description = "This is a test" }' />
view-data
The view-data attribute assigns a ViewDataDictionary to pass to the partial view. The following markup makes
the entire ViewData collection accessible to the partial view:
@{
ViewData["IsNumberReadOnly"] = true;
}
<partial name="_ProductViewDataPartial"
for="Product"
view-data="ViewData" />
In the preceding code, the IsNumberReadOnly key value is set to true and added to the ViewData collection.
Consequently, ViewData["IsNumberReadOnly"] is made accessible within the following partial view:
@model TagHelpersBuiltIn.Models.Product
<div class="form-group">
<label asp-for="Number"></label>
@if ((bool)ViewData["IsNumberReadOnly"])
{
<input asp-for="Number" type="number" class="form-control" readonly />
}
else
{
<input asp-for="Number" type="number" class="form-control" />
}
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" type="text" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Description"></label>
<textarea asp-for="Description" rows="4" cols="50" class="form-control"></textarea>
</div>
In this example, the value of ViewData["IsNumberReadOnly"] determines whether the Number field is displayed as
read only.
The following Partial Tag Helper achieves the same asynchronous rendering behavior as the PartialAsync HTML
Helper. The model attribute is assigned a Product model instance for binding to the partial view.
Additional resources
Partial views in ASP.NET Core
Views in ASP.NET Core MVC
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Provides the asp-route-<Parameter Name> attribute, where <Parameter Name> is added to the route values.
The routeValues parameters to Html.BeginForm and Html.BeginRouteForm provide similar functionality.
Has an HTML Helper alternative Html.BeginForm and Html.BeginRouteForm
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes asp-controller and
asp-action . The Form Tag Helper also generates a hidden Request Verification Token to prevent cross-site request
forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a
pure HTML Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An app with a route
named register could use the following markup for the registration page:
<form asp-route="register" method="post">
<!-- Input and Submit elements -->
</form>
Many of the views in the Views/Account folder (generated when you create a new web app with Individual User
Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an authorized resource but
are not authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the
login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table lists some
common .NET types and generated HTML type (not every .NET type is listed).
.NET TYPE INPUT TYPE
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper will map to specific
input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the model. The Input
Tag Helper consumes the model metadata and produces HTML5 data-val-* attributes (see Model Validation).
These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and jQuery
validation. The unobtrusive attributes have the format data-val-rule="Error Message" , where rule is the name of
the validation rule (such as data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is
provided in the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes of the
form data-val-ruleName-argumentName="argumentValue" that provide additional details about the rule, for example,
data-val-maxlength-max="1024" .
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named htmlAttributes when
executing their default templates. This behavior is optionally augmented using additionalViewData parameters. The
key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the htmlAttributes object
passed to input helpers like @Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression. Therefore,
asp-for="Property1" becomes m => m.Property1 in the generated code which is why you don't need to prefix with
Model . You can use the "@" character to start an inline expression and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can be expensive and
should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @ operator to access
each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended display name
might change over time, and the combination of Display attribute and Label Tag Helper will apply the
Display everywhere it's used.
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated with the <input>
element. The Tag Helpers generate consistent id and for elements so they can be correctly associated. The
caption in this sample comes from the Display attribute. If the model didn't contain a Display attribute, the
caption would be the property name of the expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same property. Doing so
displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side validation. See Model
Validation for more information.
When a server side validation error occurs (for example when you have custom server side validation or client-side
validation is disabled), MVC places that error message as the body of the <span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which generates validation
error messages on the <input> element. When a validation error occurs, the Validation Tag Helper displays the
error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and asp-items specifies
the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the Index view.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
if (ModelState.IsValid)
{
var msg = model.Country + " selected";
return RedirectToAction("IndexSuccess", new { message = msg});
}
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing
MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do
(such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem elements from the
enum values.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and "Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the
asp-for attribute is an IEnumerable . For example, given the following model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate
repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the following view and action
method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute) depending on the
current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Tag Helpers in forms in ASP.NET Core
7/24/2018 • 17 minutes to read • Edit Online
Sample:
The MVC runtime generates the action attribute value from the Form Tag Helper attributes
asp-controller and asp-action . The Form Tag Helper also generates a hidden Request
Verification Token to prevent cross-site request forgery (when used with the
[ValidateAntiForgeryToken] attribute in the HTTP Post action method). Protecting a pure HTML
Form from cross-site request forgery is difficult, the Form Tag Helper provides this service for
you.
Using a named route
The asp-route Tag Helper attribute can also generate markup for the HTML action attribute. An
app with a route named register could use the following markup for the registration page:
Many of the views in the Views/Account folder (generated when you create a new web app with
Individual User Accounts) contain the asp-route-returnurl attribute:
NOTE
With the built in templates, returnUrl is only populated automatically when you try to access an
authorized resource but are not authenticated or authorized. When you attempt an unauthorized access,
the security middleware redirects you to the login page with the returnUrl set.
Type expected
'RegisterViewModel' does not contain a definition for 'Email' and no
extension method 'Email' accepting a first argument of type 'RegisterViewModel'
could be found (are you missing a using directive or an assembly reference?)
The Input Tag Helper sets the HTML type attribute based on the .NET type. The following table
lists some common .NET types and generated HTML type (not every .NET type is listed).
Bool type=”checkbox”
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int type=”number”
The following table shows some common data annotations attributes that the input tag helper
will map to specific input types (not every validation attribute is listed):
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The data annotations applied to the Email and Password properties generate metadata on the
model. The Input Tag Helper consumes the model metadata and produces HTML5 data-val-*
attributes (see Model Validation). These attributes describe the validators to attach to the input
fields. This provides unobtrusive HTML5 and jQuery validation. The unobtrusive attributes have
the format data-val-rule="Error Message" , where rule is the name of the validation rule (such as
data-val-required , data-val-email , data-val-maxlength , etc.) If an error message is provided in
the attribute, it's displayed as the value for the data-val-rule attribute. There are also attributes
of the form data-val-ruleName-argumentName="argumentValue" that provide additional details about
the rule, for example, data-val-maxlength-max="1024" .
HTML Helper alternatives to Input Tag Helper
Html.TextBox , Html.TextBoxFor , Html.Editor and Html.EditorFor have overlapping features
with the Input Tag Helper. The Input Tag Helper will automatically set the type attribute;
Html.TextBox and Html.TextBoxFor won't. Html.Editor and Html.EditorFor handle collections,
complex objects and templates; the Input Tag Helper doesn't. The Input Tag Helper,
Html.EditorFor and Html.TextBoxFor are strongly typed (they use lambda expressions);
Html.TextBox and Html.Editor are not (they use expression names).
HtmlAttributes
@Html.Editor() and @Html.EditorFor() use a special ViewDataDictionary entry named
htmlAttributes when executing their default templates. This behavior is optionally augmented
using additionalViewData parameters. The key "htmlAttributes" is case-insensitive. The key
"htmlAttributes" is handled similarly to the htmlAttributes object passed to input helpers like
@Html.TextBox() .
Expression names
The asp-for attribute value is a ModelExpression and the right hand side of a lambda expression.
Therefore, asp-for="Property1" becomes m => m.Property1 in the generated code which is why
you don't need to prefix with Model . You can use the "@" character to start an inline expression
and move before the m. :
@{
var joe = "Joe";
}
<input asp-for="@joe" />
[DataType(DataType.Password)]
public string Password { get; set; }
@model RegisterAddressViewModel
The following Razor shows how you access a specific Color element:
@model Person
@{
var index = (int)ViewData["index"];
}
@model string
<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
@model List<ToDoItem>
</table>
<button type="submit">Save</button>
</form>
@model ToDoItem
<td>
<label asp-for="@Model.Name"></label>
@Html.DisplayFor(model => model.Name)
</td>
<td>
<input asp-for="@Model.IsDone" />
</td>
@*
This template replaces the following Razor which evaluates the indexer three times.
<td>
<label asp-for="@Model[i].Name"></label>
@Html.DisplayFor(model => model[i].Name)
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
*@
NOTE
Always use for (and not foreach ) to iterate over a list. Evaluating an indexer in a LINQ expression can
be expensive and should be minimized.
NOTE
The commented sample code above shows how you would replace the lambda expression with the @
operator to access each ToDoItem in the list.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class DescriptionViewModel
{
[MinLength(5)]
[MaxLength(1024)]
public string Description { get; set; }
}
}
@model DescriptionViewModel
The Label Tag Helper provides the following benefits over a pure HTML label element:
You automatically get the descriptive label value from the Display attribute. The intended
display name might change over time, and the combination of Display attribute and Label
Tag Helper will apply the Display everywhere it's used.
Less markup in source code
Strong typing with the model property.
Sample:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class SimpleViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
}
}
@model SimpleViewModel
The Label Tag Helper generated the for attribute value of "Email", which is the ID associated
with the <input> element. The Tag Helpers generate consistent id and for elements so they
can be correctly associated. The caption in this sample comes from the Display attribute. If the
model didn't contain a Display attribute, the caption would be the property name of the
expression.
The Validation Message Tag Helper is used with the asp-validation-for attribute on a HTML
span element.
<span asp-validation-for="Email"></span>
The Validation Message Tag Helper will generate the following HTML:
<span class="field-validation-valid"
data-valmsg-for="Email"
data-valmsg-replace="true"></span>
You generally use the Validation Message Tag Helper after an Input Tag Helper for the same
property. Doing so displays any validation error messages near the input that caused the error.
NOTE
You must have a view with the correct JavaScript and jQuery script references in place for client side
validation. See Model Validation for more information.
When a server side validation error occurs (for example when you have custom server side
validation or client-side validation is disabled), MVC places that error message as the body of the
<span> element.
The Validation Summary Tag Helper is used to display a summary of validation messages. The
asp-validation-summary attribute value can be any of the following:
ValidationSummary.ModelOnly Model
ValidationSummary.None None
Sample
In the following example, the data model is decorated with DataAnnotation attributes, which
generates validation error messages on the <input> element. When a validation error occurs, the
Validation Tag Helper displays the error message:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email Address")]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}
@model RegisterViewModel
The Select Tag Helper asp-for specifies the model property name for the select element and
asp-items specifies the option elements. For example:
Sample:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModel
{
public string Country { get; set; }
The Index method initializes the CountryViewModel , sets the selected country and passes it to the
Index view.
@model CountryViewModel
NOTE
We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more
robust at providing MVC metadata and generally less problematic.
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag
Helper attributes do (such as asp-items )
Enum binding
It's often convenient to use <select> with an enum property and generate the SelectListItem
elements from the enum values.
Sample:
public class CountryEnumViewModel
{
public CountryEnum EnumCountry { get; set; }
}
}
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
@model CountryEnumViewModel
You can decorate your enumerator list with the Display attribute to get a richer UI:
using System.ComponentModel.DataAnnotations;
namespace FormsTagHelper.ViewModels
{
public enum CountryEnum
{
[Display(Name = "United Mexican States")]
Mexico,
[Display(Name = "United States of America")]
USA,
Canada,
France,
Germany,
Spain
}
}
Option Group
The HTML <optgroup> element is generated when the view model contains one or more
SelectListGroup objects.
The CountryViewModelGroup groups the SelectListItem elements into the "North America" and
"Europe" groups:
public class CountryViewModelGroup
{
public CountryViewModelGroup()
{
var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
var EuropeGroup = new SelectListGroup { Name = "Europe" };
Multiple select
The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the
property specified in the asp-for attribute is an IEnumerable . For example, given the following
model:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace FormsTagHelper.ViewModels
{
public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }
@model CountryViewModelIEnumerable
No selection
If you find yourself using the "not specified" option in multiple pages, you can create a template to
eliminate repeating the HTML:
@model CountryViewModel
@model CountryViewModel
Adding HTML <option> elements isn't limited to the No selection case. For example, the
following view and action method will generate HTML similar to the code above:
The correct <option> element will be selected ( contain the selected="selected" attribute)
depending on the current Country value.
Additional resources
Tag Helpers
HTML Form element
Request Verification Token
Model Binding
Model Validation
IAttributeAdapter Interface
Code snippets for this document
Partial views in ASP.NET Core
9/14/2018 • 8 minutes to read • Edit Online
By Steve Smith, Luke Latham, Maher JENDOUBI, Rick Anderson, and Scott Sauber
A partial view is a Razor markup file (.cshtml) that renders HTML output within another markup file's rendered
output.
The term partial view is used when developing either an MVC app, where markup files are called views, or a
Razor Pages app, where markup files are called pages. This topic generically refers to MVC views and Razor
Pages pages as markup files.
View or download sample code (how to download)
A partial view is a .cshtml markup file maintained within the Views folder.
A controller's ViewResult is capable of returning either a view or a partial view.
Unlike MVC view rendering, a partial view doesn't run _ViewStart.cshtml. For more information on
_ViewStart.cshtml, see Layout in ASP.NET Core.
Partial view file names often begin with an underscore ( _ ). This naming convention isn't required, but it helps
to visually differentiate partial views from views.
When a file extension is present, the Tag Helper references a partial view that must be in the same folder as the
markup file calling the partial view:
The following example references a partial view from the app root. Paths that start with a tilde-slash ( ~/ ) or a
slash ( / ) refer to the app root:
Razor Pages
MVC
@await Html.PartialAsync("_PartialName")
When the file extension is present, the HTML Helper references a partial view that must be in the same folder
as the markup file calling the partial view:
@await Html.PartialAsync("_PartialName.cshtml")
The following example references a partial view from the app root. Paths that start with a tilde-slash ( ~/ ) or a
slash ( / ) refer to the app root:
Razor Pages
@await Html.PartialAsync("~/Pages/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Pages/Folder/_PartialName.cshtml")
MVC
@await Html.PartialAsync("~/Views/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Views/Folder/_PartialName.cshtml")
@await Html.PartialAsync("../Account/_LoginPartial.cshtml")
Alternatively, you can render a partial view with RenderPartialAsync. This method doesn't return an
IHtmlContent. It streams the rendered output directly to the response. Because the method doesn't return a
result, it must be called within a Razor code block:
@{
await Html.RenderPartialAsync("_AuthorPartial");
}
Since RenderPartialAsync streams rendered content, it provides better performance in some scenarios. In
performance-critical situations, benchmark the page using both approaches and use the approach that
generates a faster response.
Synchronous HTML Helper
Partial and RenderPartial are the synchronous equivalents of PartialAsync and RenderPartialAsync ,
respectively. The synchronous equivalents aren't recommended because there are scenarios in which they
deadlock. The synchronous methods are targeted for removal in a future release.
IMPORTANT
If you need to execute code, use a view component instead of a partial view.
Calling Partial or RenderPartial results in a Visual Studio analyzer warning. For example, the presence of
Partial yields the following warning message:
Use of IHtmlHelper.Partial may result in application deadlocks. Consider using <partial> Tag Helper or
IHtmlHelper.PartialAsync.
Replace calls to @Html.Partial with @await Html.PartialAsync or the Partial Tag Helper. For more information
on Partial Tag Helper migration, see Migrate from an HTML Helper.
MVC
1. /Areas/<Area-Name>/Views/<Controller-Name>
2. /Areas/<Area-Name>/Views/Shared
3. /Views/Shared
4. /Pages/Shared
1. /Areas/<Area-Name>/Views/<Controller-Name>
2. /Areas/<Area-Name>/Views/Shared
3. /Views/Shared
NOTE
A Razor section defined in a partial view is invisible to parent markup files. The section is only visible to the partial
view in which it's defined.
You can pass a model into a partial view. The model can be a custom object. You can pass a model with
PartialAsync (renders a block of content to the caller ) or RenderPartialAsync (streams the content to the
output):
Razor Pages
The following markup in the sample app is from the Pages/ArticlesRP/ReadRP.cshtml page. The page contains
two partial views. The second partial view passes in a model and ViewData to the partial view. The
ViewDataDictionary constructor overload is used to pass a new ViewData dictionary while retaining the existing
ViewData dictionary.
@model ReadRPModel
<h2>@Model.Article.Title</h2>
@* Pass the author's name to Pages\Shared\_AuthorPartialRP.cshtml *@
@await Html.PartialAsync("../Shared/_AuthorPartialRP", Model.Article.AuthorName)
@Model.Article.PublicationDate
@* Loop over the Sections and pass in a section and additional ViewData to
the strongly typed Pages\ArticlesRP\_ArticleSectionRP.cshtml partial view. *@
@{
var index = 0;
index++;
}
}
Pages/Shared/_AuthorPartialRP.cshtml is the first partial view referenced by the ReadRP.cshtml markup file:
@model string
<div>
<h3>@Model</h3>
This partial view from /Pages/Shared/_AuthorPartialRP.cshtml.
</div>
@using PartialViewsSample.ViewModels
@model ArticleSection
@model PartialViewsSample.ViewModels.Article
<h2>@Model.Title</h2>
@* Pass the author's name to Views\Shared\_AuthorPartial.cshtml *@
@await Html.PartialAsync("_AuthorPartial", Model.AuthorName)
@Model.PublicationDate
@* Loop over the Sections and pass in a section and additional ViewData to
the strongly typed Views\Articles\_ArticleSection.cshtml partial view. *@
@{
var index = 0;
index++;
}
}
Views/Shared/_AuthorPartial.cshtml is the first partial view referenced by the ReadRP.cshtml markup file:
@model string
<div>
<h3>@Model</h3>
This partial view from /Views/Shared/_AuthorPartial.cshtml.
</div>
Views/Articles/_ArticleSection.cshtml is the second partial view referenced by the Read.cshtml markup file:
@using PartialViewsSample.ViewModels
@model ArticleSection
At runtime, the partials are rendered into the parent markup file's rendered output, which itself is rendered
within the shared _Layout.cshtml. The first partial view renders the article author's name and publication date:
Abraham Lincoln
This partial view from <shared partial view file path>. 11/19/1863 12:00:00 AM
Additional resources
Razor syntax reference for ASP.NET Core
Tag Helpers in ASP.NET Core
Partial Tag Helper in ASP.NET Core
View components in ASP.NET Core
Areas in ASP.NET Core
Razor syntax reference for ASP.NET Core
View components in ASP.NET Core
Areas in ASP.NET Core
Dependency injection into views in ASP.NET Core
6/21/2018 • 4 minutes to read • Edit Online
By Steve Smith
ASP.NET Core supports dependency injection into views. This can be useful for view -specific services, such as
localization or data required only for populating view elements. You should try to maintain separation of
concerns between your controllers and views. Most of the data your views display should be passed in from the
controller.
View or download sample code (how to download)
A Simple Example
You can inject a service into a view using the @inject directive. You can think of @inject as adding a property
to your view, and populating the property using DI.
The syntax for @inject : @inject <type> <name>
@using System.Threading.Tasks
@using ViewInjectSample.Model
@using ViewInjectSample.Model.Services
@model IEnumerable<ToDoItem>
@inject StatisticsService StatsService
<!DOCTYPE html>
<html>
<head>
<title>To Do Items</title>
</head>
<body>
<div>
<h1>To Do Items</h1>
<ul>
<li>Total Items: @StatsService.GetCount()</li>
<li>Completed: @StatsService.GetCompletedCount()</li>
<li>Avg. Priority: @StatsService.GetAveragePriority()</li>
</ul>
<table>
<tr>
<th>Name</th>
<th>Priority</th>
<th>Is Done?</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>@item.Name</td>
<td>@item.Priority</td>
<td>@item.IsDone</td>
</tr>
}
</table>
</div>
</body>
</html>
This view displays a list of ToDoItem instances, along with a summary showing overall statistics. The summary is
populated from the injected StatisticsService . This service is registered for dependency injection in
ConfigureServices in Startup.cs:
services.AddTransient<IToDoItemRepository, ToDoItemRepository>();
services.AddTransient<StatisticsService>();
services.AddTransient<ProfileOptionsService>();
The StatisticsService performs some calculations on the set of ToDoItem instances, which it accesses via a
repository:
using System.Linq;
using ViewInjectSample.Interfaces;
namespace ViewInjectSample.Model.Services
{
public class StatisticsService
{
private readonly IToDoItemRepository _toDoItemRepository;
The sample repository uses an in-memory collection. The implementation shown above (which operates on all of
the data in memory) isn't recommended for large, remotely accessed data sets.
The sample displays data from the model bound to the view and the service injected into the view:
Populating Lookup Data
View injection can be useful to populate options in UI elements, such as dropdown lists. Consider a user profile
form that includes options for specifying gender, state, and other preferences. Rendering such a form using a
standard MVC approach would require the controller to request data access services for each of these sets of
options, and then populate a model or ViewBag with each set of options to be bound.
An alternative approach injects services directly into the view to obtain the options. This minimizes the amount
of code required by the controller, moving this view element construction logic into the view itself. The controller
action to display a profile editing form only needs to pass the form the profile instance:
using Microsoft.AspNetCore.Mvc;
using ViewInjectSample.Model;
namespace ViewInjectSample.Controllers
{
public class ProfileController : Controller
{
[Route("Profile")]
public IActionResult Index()
{
// TODO: look up profile based on logged-in user
var profile = new Profile()
{
Name = "Steve",
FavColor = "Blue",
Gender = "Male",
State = new State("Ohio","OH")
};
return View(profile);
}
}
}
The HTML form used to update these preferences includes dropdown lists for three of the properties:
These lists are populated by a service that has been injected into the view:
@using System.Threading.Tasks
@using ViewInjectSample.Model.Services
@model ViewInjectSample.Model.Profile
@inject ProfileOptionsService Options
<!DOCTYPE html>
<html>
<head>
<title>Update Profile</title>
</head>
<body>
<div>
<h1>Update Profile</h1>
Name: @Html.TextBoxFor(m => m.Name)
<br/>
Gender: @Html.DropDownList("Gender",
Options.ListGenders().Select(g =>
new SelectListItem() { Text = g, Value = g }))
<br/>
The ProfileOptionsService is a UI-level service designed to provide just the data needed for this form:
using System.Collections.Generic;
namespace ViewInjectSample.Model.Services
{
public class ProfileOptionsService
{
public List<string> ListGenders()
{
// keeping this simple
return new List<string>() {"Female", "Male"};
}
IMPORTANT
Don't forget to register types you request through dependency injection in Startup.ConfigureServices . An
unregistered type throws an exception at runtime because the service provider is internally queried via GetRequiredService.
Overriding Services
In addition to injecting new services, this technique can also be used to override previously injected services on a
page. The figure below shows all of the fields available on the page used in the first example:
As you can see, the default fields include Html , Component , and Url (as well as the StatsService that we
injected). If for instance you wanted to replace the default HTML Helpers with your own, you could easily do so
using @inject :
@using System.Threading.Tasks
@using ViewInjectSample.Helpers
@inject MyHtmlHelper Html
<!DOCTYPE html>
<html>
<head>
<title>My Helper</title>
</head>
<body>
<div>
Test: @Html.Value
</div>
</body>
</html>
If you want to extend existing services, you can simply use this technique while inheriting from or wrapping the
existing implementation with your own.
See Also
Simon Timms Blog: Getting Lookup Data Into Your View
View components in ASP.NET Core
9/26/2018 • 10 minutes to read • Edit Online
By Rick Anderson
View or download sample code (how to download)
View components
View components are similar to partial views, but they're much more powerful. View components don't use
model binding, and only depend on the data provided when calling into it. This article was written using
ASP.NET Core MVC, but view components also work with Razor Pages.
A view component:
Renders a chunk rather than a whole response.
Includes the same separation-of-concerns and testability benefits found between a controller and view.
Can have parameters and business logic.
Is typically invoked from a layout page.
View components are intended anywhere you have reusable rendering logic that's too complex for a partial
view, such as:
Dynamic navigation menus
Tag cloud (where it queries the database)
Login panel
Shopping cart
Recently published articles
Sidebar content on a typical blog
A login panel that would be rendered on every page and show either the links to log out or log in, depending
on the log in state of the user
A view component consists of two parts: the class (typically derived from ViewComponent) and the result it
returns (typically a view ). Like controllers, a view component can be a POCO, but most developers will want to
take advantage of the methods and properties available by deriving from ViewComponent .
The parameters will be passed to the InvokeAsync method. The PriorityList view component developed in the
article is invoked from the Views/Todo/Index.cshtml view file. In the following, the InvokeAsync method is called
with two parameters:
Pascal-cased class and method parameters for Tag Helpers are translated into their lower kebab case. The Tag
Helper to invoke a view component uses the <vc></vc> element. The view component is specified as follows:
<vc:[view-component-name]
parameter1="parameter1 value"
parameter2="parameter2 value">
</vc:[view-component-name]>
To use a view component as a Tag Helper, register the assembly containing the view component using the
@addTagHelper directive. If your view component is in an assembly called MyWebApp , add the following directive
to the _ViewImports.cshtml file:
@addTagHelper *, MyWebApp
You can register a view component as a Tag Helper to any file that references the view component. See
Managing Tag Helper Scope for more information on how to register Tag Helpers.
The InvokeAsync method used in this tutorial:
In the sample above, the PriorityList view component becomes priority-list . The parameters to the view
component are passed as attributes in lower kebab case.
Invoking a view component directly from a controller
View components are typically invoked from a view, but you can invoke them directly from a controller method.
While view components don't define endpoints like controllers, you can easily implement a controller action that
returns the content of a ViewComponentResult .
In this example, the view component is called directly from the controller:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents
{
public class PriorityListViewComponent : ViewComponent
{
private readonly ToDoContext db;
[ViewComponent(Name = "PriorityList")]
public class XYZ : ViewComponent
The [ViewComponent] attribute above tells the view component selector to use the name PriorityList
when looking for the views associated with the component, and to use the string "PriorityList" when
referencing the class component from a view. I'll explain that in more detail later.
The component uses dependency injection to make the data context available.
InvokeAsync exposes a method which can be called from a view, and it can take an arbitrary number of
arguments.
The InvokeAsync method returns the set of ToDo items that satisfy the isDone and maxPriority
parameters.
Create the view component Razor view
Create the Views/Shared/Components folder. This folder must be named Components.
Create the Views/Shared/Components/PriorityList folder. This folder name must match the name of the
view component class, or the name of the class minus the suffix (if we followed convention and used the
ViewComponent suffix in the class name). If you used the ViewComponent attribute, the class name would
need to match the attribute designation.
Create a Views/Shared/Components/PriorityList/Default.cshtml Razor view:
@model IEnumerable<ViewComponentSample.Models.TodoItem>
<h3>Priority Items</h3>
<ul>
@foreach (var todo in Model)
{
<li>@todo.Name</li>
}
</ul>
The Razor view takes a list of TodoItem and displays them. If the view component InvokeAsync method
doesn't pass the name of the view (as in our sample), Default is used for the view name by convention.
Later in the tutorial, I'll show you how to pass the name of the view. To override the default styling for a
specific controller, add a view to the controller-specific view folder (for example
Views/Todo/Components/PriorityList/Default.cshtml).
If the view component is controller-specific, you can add it to the controller-specific folder
(Views/Todo/Components/PriorityList/Default.cshtml).
Add a div containing a call to the priority list component to the bottom of the Views/Todo/index.cshtml
file:
</table>
<div>
@await Component.InvokeAsync("PriorityList", new { maxPriority = 2, isDone = false })
</div>
The markup @await Component.InvokeAsync shows the syntax for calling view components. The first argument is
the name of the component we want to invoke or call. Subsequent parameters are passed to the component.
InvokeAsync can take an arbitrary number of arguments.
Test the app. The following image shows the ToDo list and the priority items:
You can also call the view component directly from the controller:
@model IEnumerable<ViewComponentSample.Models.TodoItem>
Update Views/TodoList/Index.cshtml:
Copy Views/Todo/Components/PriorityList/1Default.cshtml to
Views/Shared/Components/PriorityList/Default.cshtml.
Add some markup to the Shared Todo view component view to indicate the view is from the Shared
folder.
Test the Shared component view.
Avoiding magic strings
If you want compile time safety, you can replace the hard-coded view component name with the class name.
Create the view component without the "ViewComponent" suffix:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents
{
public class PriorityList : ViewComponent
{
private readonly ToDoContext db;
Add a using statement to your Razor view file, and use the nameof operator:
@using ViewComponentSample.Models
@using ViewComponentSample.ViewComponents
@model IEnumerable<TodoItem>
<h2>ToDo nameof</h2>
<!-- Markup removed for brevity. -->
<div>
The view component's Razor file lists the strings passed to the Invoke method
(Views/Home/Components/PriorityList/Default.cshtml):
@model List<string>
<h3>Priority Items</h3>
<ul>
@foreach (var item in Model)
{
<li>@item</li>
}
</ul>
The view component is invoked in a Razor file (for example, Views/Home/Index.cshtml) using one of the
following approaches:
IViewComponentHelper
Tag Helper
To use the IViewComponentHelper approach, call Component.InvokeAsync :
The view component is invoked in a Razor file (for example, Views/Home/Index.cshtml) with
IViewComponentHelper.
Call Component.InvokeAsync :
To use the Tag Helper, register the assembly containing the View Component using the @addTagHelper directive
(the view component is in an assembly called MyWebApp ):
@addTagHelper *, MyWebApp
Use the view component Tag Helper in the Razor markup file:
The method signature of PriorityList.Invoke is synchronous, but Razor finds and calls the method with
Component.InvokeAsync in the markup file.
Additional resources
Dependency injection into views
Handle requests with controllers in ASP.NET Core
MVC
6/21/2018 • 5 minutes to read • Edit Online
What is a Controller?
A controller is used to define and group a set of actions. An action (or action method) is a method on a controller
which handles requests. Controllers logically group similar actions together. This aggregation of actions allows
common sets of rules, such as routing, caching, and authorization, to be applied collectively. Requests are mapped
to actions through routing.
By convention, controller classes:
Reside in the project's root-level Controllers folder
Inherit from Microsoft.AspNetCore.Mvc.Controller
A controller is an instantiable class in which at least one of the following conditions is true:
The class name is suffixed with "Controller"
The class inherits from a class whose name is suffixed with "Controller"
The class is decorated with the [Controller] attribute
A controller class must not have an associated [NonController] attribute.
Controllers should follow the Explicit Dependencies Principle. There are a couple of approaches to implementing
this principle. If multiple controller actions require the same service, consider using constructor injection to
request those dependencies. If the service is needed by only a single action method, consider using Action
Injection to request the dependency.
Within the Model-View -Controller pattern, a controller is responsible for the initial processing of the request and
instantiation of the model. Generally, business decisions should be performed within the model.
The controller takes the result of the model's processing (if any) and returns either the proper view and its
associated view data or the result of the API call. Learn more at Overview of ASP.NET Core MVC and Get started
with ASP.NET Core MVC and Visual Studio.
The controller is a UI -level abstraction. Its responsibilities are to ensure request data is valid and to choose which
view (or result for an API) should be returned. In well-factored apps, it doesn't directly include data access or
business logic. Instead, the controller delegates to services handling these responsibilities.
Defining Actions
Public methods on a controller, except those decorated with the [NonAction] attribute, are actions. Parameters on
actions are bound to request data and are validated using model binding. Model validation occurs for everything
that's model-bound. The ModelState.IsValid property value indicates whether model binding and validation
succeeded.
Action methods should contain logic for mapping a request to a business concern. Business concerns should
typically be represented as services that the controller accesses through dependency injection. Actions then map
the result of the business action to an application state.
Actions can return anything, but frequently return an instance of IActionResult (or Task<IActionResult> for async
methods) that produces a response. The action method is responsible for choosing what kind of response. The
action result does the responding.
Controller Helper Methods
Controllers usually inherit from Controller, although this isn't required. Deriving from Controller provides access
to three categories of helper methods:
1. Methods resulting in an empty response body
No Content-Type HTTP response header is included, since the response body lacks content to describe.
There are two result types within this category: Redirect and HTTP Status Code.
HTTP Status Code
This type returns an HTTP status code. A couple of helper methods of this type are BadRequest , NotFound ,
and Ok . For example, return BadRequest(); produces a 400 status code when executed. When methods
such as BadRequest , NotFound , and Ok are overloaded, they no longer qualify as HTTP Status Code
responders, since content negotiation is taking place.
Redirect
This type returns a redirect to an action or destination (using Redirect , LocalRedirect , RedirectToAction ,
or RedirectToRoute ). For example, return RedirectToAction("Complete", new {id = 123}); redirects to
Complete , passing an anonymous object.
The Redirect result type differs from the HTTP Status Code type primarily in the addition of a Location
HTTP response header.
2. Methods resulting in a non-empty response body with a predefined content type
Most helper methods in this category include a ContentType property, allowing you to set the Content-Type
response header to describe the response body.
There are two result types within this category: View and Formatted Response.
View
This type returns a view which uses a model to render HTML. For example, return View(customer); passes
a model to the view for data-binding.
Formatted Response
This type returns JSON or a similar data exchange format to represent an object in a specific manner. For
example, return Json(customer); serializes the provided object into JSON format.
Other common methods of this type include File , PhysicalFile, and VirtualFile . For example,
return PhysicalFile(customerFilePath, "text/xml"); returns an XML file described by a Content-Type
response header value of "text/xml".
3. Methods resulting in a non-empty response body formatted in a content type negotiated with the client
This category is better known as Content Negotiation. Content negotiation applies whenever an action returns
an ObjectResult type or something other than an IActionResult implementation. An action that returns a non-
IActionResult implementation (for example, object ) also returns a Formatted Response.
Some helper methods of this type include BadRequest , CreatedAtRoute , and Ok . Examples of these methods
include return BadRequest(modelState); , return CreatedAtRoute("routename", values, newobject); , and
return Ok(value); , respectively. Note that BadRequest and Ok perform content negotiation only when passed a
value; without being passed a value, they instead serve as HTTP Status Code result types. The CreatedAtRoute
method, on the other hand, always performs content negotiation since its overloads all require that a value be
passed.
Cross-Cutting Concerns
Applications typically share parts of their workflow. Examples include an app that requires authentication to access
the shopping cart, or an app that caches data on some pages. To perform logic before or after an action method,
use a filter. Using Filters on cross-cutting concerns can reduce duplication, allowing them to follow the Don't
Repeat Yourself (DRY ) principle.
Most filter attributes, such as [Authorize] , can be applied at the controller or action level depending upon the
desired level of granularity.
Error handling and response caching are often cross-cutting concerns:
Handle errors
Response Caching
Many cross-cutting concerns can be handled using filters or custom middleware.
Routing to controller actions in ASP.NET Core
9/18/2018 • 30 minutes to read • Edit Online
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
Inside the call to UseMvc , MapRoute is used to create a single route, which we'll refer to as the default
route. Most MVC apps will use a route with a template similar to the default route.
The route template "{controller=Home}/{action=Index}/{id?}" can match a URL path like
/Products/Details/5 and will extract the route values { controller = Products, action = Details, id = 5 }
by tokenizing the path. MVC will attempt to locate a controller named ProductsController and run the
action Details :
Note that in this example, model binding would use the value of id = 5 to set the id parameter to 5
when invoking this action. See the Model Binding for more details.
Using the default route:
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
Using this controller definition and route template, the HomeController.Index action would be executed for
any of the following URL paths:
/Home/Index/17
/Home/Index
/Home
app.UseMvcWithDefaultRoute();
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
UseMvc and UseMvcWithDefaultRoute add an instance of RouterMiddleware to the middleware pipeline. MVC
doesn't interact directly with middleware, and uses routing to handle requests. MVC is connected to the
routes through an instance of MvcRouteHandler . The code inside of UseMvc is similar to the following:
UseMvc doesn't directly define any routes, it adds a placeholder to the route collection for the attribute
route. The overload UseMvc(Action<IRouteBuilder>) lets you add your own routes and also supports attribute
routing. UseMvc and all of its variations adds a placeholder for the attribute route - attribute routing is
always available regardless of how you configure UseMvc . UseMvcWithDefaultRoute defines a default route
and supports attribute routing. The Attribute Routing section includes more details on attribute routing.
Conventional routing
The default route:
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
is an example of a conventional routing. We call this style conventional routing because it establishes a
convention for URL paths:
the first path segment maps to the controller name
the second maps to the action name.
the third segment is used for an optional id used to map to a model entity
Using this route, the URL path /Products/List maps to the ProductsController.List action, and
default
/Blog/Article/17 maps to BlogController.Article . This mapping is based on the controller and action
names only and isn't based on namespaces, source file locations, or method parameters.
TIP
Using conventional routing with the default route allows you to build the application quickly without having to come
up with a new URL pattern for each action you define. For an application with CRUD style actions, having consistency
for the URLs across your controllers can help simplify your code and make your UI more predictable.
WARNING
The id is defined as optional by the route template, meaning that your actions can execute without the ID provided
as part of the URL. Usually what will happen if id is omitted from the URL is that it will be set to 0 by model
binding, and as a result no entity will be found in the database matching id == 0 . Attribute routing can give you
fine-grained control to make the ID required for some actions and not for others. By convention the documentation
will include optional parameters like id when they're likely to appear in correct usage.
Multiple routes
You can add multiple routes inside UseMvc by adding more calls to MapRoute . Doing so allows you to define
multiple conventions, or to add conventional routes that are dedicated to a specific action, such as:
app.UseMvc(routes =>
{
routes.MapRoute("blog", "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
The blog route here is a dedicated conventional route, meaning that it uses the conventional routing
system, but is dedicated to a specific action. Since controller and action don't appear in the route
template as parameters, they can only have the default values, and thus this route will always map to the
action BlogController.Article .
Routes in the route collection are ordered, and will be processed in the order they're added. So in this
example, the blog route will be tried before the default route.
NOTE
Dedicated conventional routes often use catch-all route parameters like {*article} to capture the remaining
portion of the URL path. This can make a route 'too greedy' meaning that it matches URLs that you intended to be
matched by other routes. Put the 'greedy' routes later in the route table to solve this.
Fallback
As part of request processing, MVC will verify that the route values can be used to find a controller and
action in your application. If the route values don't match an action then the route isn't considered a match,
and the next route will be tried. This is called fallback, and it's intended to simplify cases where conventional
routes overlap.
Disambiguating actions
When two actions match through routing, MVC must disambiguate to choose the 'best' candidate or else
throw an exception. For example:
[HttpPost]
public IActionResult Edit(int id, Product product) { ... }
}
This controller defines two actions that would match the URL path /Products/Edit/17 and route data
{ controller = Products, action = Edit, id = 17 } . This is a typical pattern for MVC controllers where
Edit(int) shows a form to edit a product, and Edit(int, Product) processes the posted form. To make this
possible MVC would need to choose Edit(int, Product) when the request is an HTTP POST and
Edit(int) when the HTTP verb is anything else.
The HttpPostAttribute ( [HttpPost] ) is an implementation of IActionConstraint that will only allow the
action to be selected when the HTTP verb is POST . The presence of an IActionConstraint makes the
Edit(int, Product) a 'better' match than Edit(int) , so Edit(int, Product) will be tried first.
You will only need to write custom IActionConstraint implementations in specialized scenarios, but it's
important to understand the role of attributes like HttpPostAttribute - similar attributes are defined for
other HTTP verbs. In conventional routing it's common for actions to use the same action name when
they're part of a show form -> submit form workflow. The convenience of this pattern will become more
apparent after reviewing the Understanding IActionConstraint section.
If multiple routes match, and MVC can't find a 'best' route, it will throw an AmbiguousActionException .
Route names
The strings "blog" and "default" in the following examples are route names:
app.UseMvc(routes =>
{
routes.MapRoute("blog", "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
The route names give the route a logical name so that the named route can be used for URL generation.
This greatly simplifies URL creation when the ordering of routes could make URL generation complicated.
Route names must be unique application-wide.
Route names have no impact on URL matching or handling of requests; they're used only for URL
generation. Routing has more detailed information on URL generation including URL generation in MVC -
specific helpers.
Attribute routing
Attribute routing uses a set of attributes to map actions directly to route templates. In the following example,
app.UseMvc(); is used in the Configure method and no route is passed. The HomeController will match a
set of URLs similar to what the default route {controller=Home}/{action=Index}/{id?} would match:
The HomeController.Index() action will be executed for any of the URL paths / , /Home , or /Home/Index .
NOTE
This example highlights a key programming difference between attribute routing and conventional routing. Attribute
routing requires more input to specify a route; the conventional default route handles routes more succinctly.
However, attribute routing allows (and requires) precise control of which route templates apply to each action.
With attribute routing the controller name and action names play no role in which action is selected. This
example will match the same URLs as the previous example.
public class MyDemoController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
public IActionResult MyIndex()
{
return View("Index");
}
[Route("Home/About")]
public IActionResult MyAbout()
{
return View("About");
}
[Route("Home/Contact")]
public IActionResult MyContact()
{
return View("Contact");
}
}
NOTE
The route templates above don't define route parameters for action , area , and controller . In fact, these route
parameters are not allowed in attribute routes. Since the route template is already associated with an action, it
wouldn't make sense to parse the action name from the URL.
[HttpGet("/products")]
public IActionResult ListProducts()
{
// ...
}
[HttpPost("/products")]
public IActionResult CreateProduct(...)
{
// ...
}
For a URL path like /products the ProductsApi.ListProducts action will be executed when the HTTP verb is
GET and ProductsApi.CreateProduct will be executed when the HTTP verb is POST . Attribute routing first
matches the URL against the set of route templates defined by route attributes. Once a route template
matches, IActionConstraint constraints are applied to determine which actions can be executed.
TIP
When building a REST API, it's rare that you will want to use [Route(...)] on an action method. It's better to use
the more specific Http*Verb*Attributes to be precise about what your API supports. Clients of REST APIs are
expected to know what paths and HTTP verbs map to specific logical operations.
Since an attribute route applies to a specific action, it's easy to make parameters required as part of the route
template definition. In this example, id is required as part of the URL path.
public class ProductsApiController : Controller
{
[HttpGet("/products/{id}", Name = "Products_List")]
public IActionResult GetProduct(int id) { ... }
}
The ProductsApi.GetProduct(int) action will be executed for a URL path like /products/3 but not for a URL
path like /products . See Routing for a full description of route templates and related options.
Route Name
The following code defines a route name of Products_List :
Route names can be used to generate a URL based on a specific route. Route names have no impact on the
URL matching behavior of routing and are only used for URL generation. Route names must be unique
application-wide.
NOTE
Contrast this with the conventional default route, which defines the id parameter as optional ( {id?} ). This ability
to precisely specify APIs has advantages, such as allowing /products and /products/5 to be dispatched to
different actions.
Combining routes
To make attribute routing less repetitive, route attributes on the controller are combined with route
attributes on the individual actions. Any route templates defined on the controller are prepended to route
templates on the actions. Placing a route attribute on the controller makes all actions in the controller use
attribute routing.
[Route("products")]
public class ProductsApiController : Controller
{
[HttpGet]
public IActionResult ListProducts() { ... }
[HttpGet("{id}")]
public ActionResult GetProduct(int id) { ... }
}
In this example the URL path /products can match ProductsApi.ListProducts , and the URL path
/products/5 can match ProductsApi.GetProduct(int) . Both of these actions only match HTTP GET because
they're decorated with the HttpGetAttribute .
Route templates applied to an action that begin with a / don't get combined with route templates applied
to the controller. This example matches a set of URL paths similar to the default route.
[Route("Home")]
public class HomeController : Controller
{
[Route("")] // Combines to define the route template "Home"
[Route("Index")] // Combines to define the route template "Home/Index"
[Route("/")] // Doesn't combine, defines the route template ""
public IActionResult Index()
{
ViewData["Message"] = "Home index";
var url = Url.Action("Index", "Home");
ViewData["Message"] = "Home index" + "var url = Url.Action; = " + url;
return View();
}
TIP
Avoid depending on Order . If your URL-space requires explicit order values to route correctly, then it's likely
confusing to clients as well. In general attribute routing will select the correct route with URL matching. If the default
order used for URL generation isn't working, using route name as an override is usually simpler than applying the
Order property.
Razor Pages routing and MVC controller routing share an implementation. Information on route order in
the Razor Pages topics is available at Razor Pages route and app conventions: Route order.
Token replacement occurs as the last step of building the attribute routes. The above example will behave the
same as the following code:
Attribute routes can also be combined with inheritance. This is particularly powerful combined with token
replacement.
[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }
Putting multiple route attributes on the controller means that each one will combine with each of the route
attributes on the action methods.
[Route("Store")]
[Route("[controller]")]
public class ProductsController : Controller
{
[HttpPost("Buy")] // Matches 'Products/Buy' and 'Store/Buy'
[HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'
public IActionResult Buy()
}
When multiple route attributes (that implement IActionConstraint ) are placed on an action, then each
action constraint combines with the route template from the attribute that defined it.
[Route("api/[controller]")]
public class ProductsController : Controller
{
[HttpPut("Buy")] // Matches PUT 'api/Products/Buy'
[HttpPost("Checkout")] // Matches POST 'api/Products/Checkout'
public IActionResult Buy()
}
TIP
While using multiple routes on actions can seem powerful, it's better to keep your application's URL space simple and
well-defined. Use multiple routes on actions only where needed, for example to support existing clients.
[HttpPost("product/{id:int}")]
public IActionResult ShowProduct(int id)
{
// ...
}
See Route Template Reference for a detailed description of route template syntax.
Custom route attributes using IRouteTemplateProvider
All of the route attributes provided in the framework ( [Route(...)] , [HttpGet(...)] , etc.) implement the
IRouteTemplateProvider interface. MVC looks for attributes on controller classes and action methods when
the app starts and uses the ones that implement IRouteTemplateProvider to build the initial set of routes.
You can implement IRouteTemplateProvider to define your own route attributes. Each
IRouteTemplateProvider allows you to define a single route with a custom route template, order, and name:
public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
public string Template => "api/[controller]";
The attribute from the above example automatically sets the Template to "api/[controller]" when
[MyApiController] is applied.
// Use the namespace and controller name to infer a route for the controller.
//
// Example:
//
// controller.ControllerTypeInfo -> "My.Application.Admin.UsersController"
// baseNamespace -> "My.Application"
//
// template => "Admin/[controller]"
//
// This makes your routes roughly line up with the folder structure of your project.
//
var namespc = controller.ControllerType.Namespace;
if (namespc == null)
return;
var template = new StringBuilder();
template.Append(namespc, _baseNamespace.Length + 1,
namespc.Length - _baseNamespace.Length - 1);
template.Replace('.', '/');
template.Append("/[controller]");
URL Generation
MVC applications can use routing's URL generation features to generate URL links to actions. Generating
URLs eliminates hardcoding URLs, making your code more robust and maintainable. This section focuses
on the URL generation features provided by MVC and will only cover basics of how URL generation works.
See Routing for a detailed description of URL generation.
The IUrlHelper interface is the underlying piece of infrastructure between MVC and routing for URL
generation. You'll find an instance of IUrlHelper available through the Url property in controllers, views,
and view components.
In this example, the IUrlHelper interface is used through the Controller.Url property to generate a URL to
another action.
using Microsoft.AspNetCore.Mvc;
If the application is using the default conventional route, the value of the url variable will be the URL path
string /UrlGeneration/Destination . This URL path is created by routing by combining the route values from
the current request (ambient values), with the values passed to Url.Action and substituting those values
into the route template:
result: /UrlGeneration/Destination
Each route parameter in the route template has its value substituted by matching names with the values and
ambient values. A route parameter that doesn't have a value can use a default value if it has one, or be
skipped if it's optional (as in the case of id in this example). URL generation will fail if any required route
parameter doesn't have a corresponding value. If URL generation fails for a route, the next route is tried until
all routes have been tried or a match is found.
The example of Url.Action above assumes conventional routing, but URL generation works similarly with
attribute routing, though the concepts are different. With conventional routing, the route values are used to
expand a template, and the route values for controller and action usually appear in that template - this
works because the URLs matched by routing adhere to a convention. In attribute routing, the route values
for controller and action are not allowed to appear in the template - they're instead used to look up
which template to use.
This example uses attribute routing:
// In Startup class
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
using Microsoft.AspNetCore.Mvc;
[HttpGet("custom/url/to/destination")]
public IActionResult Destination() {
return View();
}
}
MVC builds a lookup table of all attribute routed actions and will match the controller and action values
to select the route template to use for URL generation. In the sample above, custom/url/to/destination is
generated.
Generating URLs by action name
Url.Action ( IUrlHelper . Action ) and all related overloads all are based on that idea that you want to
specify what you're linking to by specifying a controller name and action name.
NOTE
When using Url.Action , the current route values for controller and action are specified for you - the value of
controller and action are part of both ambient values and values. The method Url.Action , always uses the
current values of action and controller and will generate a URL path that routes to the current action.
Routing attempts to use the values in ambient values to fill in information that you didn't provide when
generating a URL. Using a route like {a}/{b}/{c}/{d} and ambient values
{ a = Alice, b = Bob, c = Carol, d = David } , routing has enough information to generate a URL without
any additional values - since all route parameters have a value. If you added the value { d = Donovan } , the
value { d = David } would be ignored, and the generated URL path would be Alice/Bob/Carol/Donovan .
WARNING
URL paths are hierarchical. In the example above, if you added the value { c = Cheryl } , both of the values
{ c = Carol, d = David } would be ignored. In this case we no longer have a value for d and URL generation
will fail. You would need to specify the desired value of c and d . You might expect to hit this problem with the
default route ( {controller}/{action}/{id?} ) - but you will rarely encounter this behavior in practice as
Url.Action will always explicitly specify a controller and action value.
Longer overloads of Url.Action also take an additional route values object to provide values for route
parameters other than controller and action . You will most commonly see this used with id like
Url.Action("Buy", "Products", new { id = 17 }) . By convention the route values object is usually an object
of anonymous type, but it can also be an IDictionary<> or a plain old .NET object. Any additional route
values that don't match route parameters are put in the query string.
using Microsoft.AspNetCore.Mvc;
TIP
To create an absolute URL, use an overload that accepts a protocol :
Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme)
using Microsoft.AspNetCore.Mvc;
TagHelpers generate URLs through the form TagHelper and the <a> TagHelper. Both of these use
IUrlHelper for their implementation. See Working with Forms for more information.
Inside views, the IUrlHelper is available through the Url property for any ad-hoc URL generation not
covered by the above.
Generating URLS in Action Results
The examples above have shown using IUrlHelper in a controller, while the most common usage in a
controller is to generate a URL as part of an action result.
The ControllerBase and Controller base classes provide convenience methods for action results that
reference another action. One typical usage is to redirect after accepting user input.
The action results factory methods follow a similar pattern to the methods on IUrlHelper .
Special case for dedicated conventional routes
Conventional routing can use a special kind of route definition called a dedicated conventional route. In the
example below, the route named blog is a dedicated conventional route.
app.UseMvc(routes =>
{
routes.MapRoute("blog", "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
Using these route definitions, Url.Action("Index", "Home") will generate the URL path / with the default
route, but why? You might guess the route values { controller = Home, action = Index } would be enough
to generate a URL using blog , and the result would be /blog?action=Index&controller=Home .
Dedicated conventional routes rely on a special behavior of default values that don't have a corresponding
route parameter that prevents the route from being "too greedy" with URL generation. In this case the
default values are { controller = Blog, action = Article } , and neither controller nor action appears as
a route parameter. When routing performs URL generation, the values provided must match the default
values. URL generation using blog will fail because the values { controller = Home, action = Index } don't
match { controller = Blog, action = Article } . Routing then falls back to try default , which succeeds.
Areas
Areas are an MVC feature used to organize related functionality into a group as a separate routing-
namespace (for controller actions) and folder structure (for views). Using areas allows an application to have
multiple controllers with the same name - as long as they have different areas. Using areas creates a
hierarchy for the purpose of routing by adding another route parameter, area to controller and action .
This section will discuss how routing interacts with areas - see Areas for details about how areas are used
with views.
The following example configures MVC to use the default conventional route and an area route for an area
named Blog :
app.UseMvc(routes =>
{
routes.MapAreaRoute("blog_route", "Blog",
"Manage/{controller}/{action}/{id?}");
routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});
When matching a URL path like , the first route will produce the route values
/Manage/Users/AddUser
{ area = Blog, controller = Users, action = AddUser } . The area route value is produced by a default
value for area , in fact the route created by MapAreaRoute is equivalent to the following:
app.UseMvc(routes =>
{
routes.MapRoute("blog_route", "Manage/{controller}/{action}/{id?}",
defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});
MapAreaRoute creates a route using both a default value and constraint for area using the provided area
name, in this case Blog . The default value ensures that the route always produces { area = Blog, ... } ,
the constraint requires the value { area = Blog, ... } for URL generation.
TIP
Conventional routing is order-dependent. In general, routes with areas should be placed earlier in the route table as
they're more specific than routes without an area.
Using the above example, the route values would match the following action:
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
The AreaAttribute is what denotes a controller as part of an area, we say that this controller is in the Blog
area. Controllers without an [Area] attribute are not members of any area, and will not match when the
area route value is provided by routing. In the following example, only the first controller listed can match
the route values { area = Blog, controller = Users, action = AddUser } .
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace1
{
[Area("Blog")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace2
{
// Matches { area = Zebra, controller = Users, action = AddUser }
[Area("Zebra")]
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace3
{
// Matches { area = string.Empty, controller = Users, action = AddUser }
// Matches { area = null, controller = Users, action = AddUser }
// Matches { controller = Users, action = AddUser }
public class UsersController : Controller
{
public IActionResult AddUser()
{
return View();
}
}
}
NOTE
The namespace of each controller is shown here for completeness - otherwise the controllers would have a naming
conflict and generate a compiler error. Class namespaces have no effect on MVC's routing.
The first two controllers are members of areas, and only match when their respective area name is provided
by the area route value. The third controller isn't a member of any area, and can only match when no value
for area is provided by routing.
NOTE
In terms of matching no value, the absence of the area value is the same as if the value for area were null or the
empty string.
When executing an action inside an area, the route value for area will be available as an ambient value for
routing to use for URL generation. This means that by default areas act sticky for URL generation as
demonstrated by the following sample.
app.UseMvc(routes =>
{
routes.MapAreaRoute("duck_route", "Duck",
"Manage/{controller}/{action}/{id?}");
routes.MapRoute("default", "Manage/{controller=Home}/{action=Index}/{id?}");
});
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Namespace4
{
[Area("Duck")]
public class UsersController : Controller
{
public IActionResult GenerateURLInArea()
{
// Uses the 'ambient' value of area
var url = Url.Action("Index", "Home");
// returns /Manage
return Content(url);
}
Understanding IActionConstraint
NOTE
This section is a deep-dive on framework internals and how MVC chooses an action to execute. A typical application
won't need a custom IActionConstraint
You have likely already used IActionConstraint even if you're not familiar with the interface. The [HttpGet]
Attribute and similar [Http-VERB] attributes implement IActionConstraint in order to limit the execution of
an action method.
public class ProductsController : Controller
{
[HttpGet]
public IActionResult Edit() { }
Assuming the default conventional route, the URL path /Products/Edit would produce the values
{ controller = Products, action = Edit } , which would match both of the actions shown here. In
IActionConstraint terminology we would say that both of these actions are considered candidates - as they
both match the route data.
When the HttpGetAttribute executes, it will say that Edit() is a match for GET and isn't a match for any other
HTTP verb. The Edit(...) action doesn't have any constraints defined, and so will match any HTTP verb. So
assuming a POST - only Edit(...) matches. But, for a GET both actions can still match - however, an action
with an IActionConstraint is always considered better than an action without. So because Edit() has
[HttpGet] it's considered more specific, and will be selected if both actions can match.
Conceptually, IActionConstraint is a form of overloading, but instead of overloading methods with the
same name, it's overloading between actions that match the same URL. Attribute routing also uses
IActionConstraint and can result in actions from different controllers both being considered candidates.
Implementing IActionConstraint
The simplest way to implement an IActionConstraint is to create a class derived from System.Attribute
and place it on your actions and controllers. MVC will automatically discover any IActionConstraint that are
applied as attributes. You can use the application model to apply constraints, and this is probably the most
flexible approach as it allows you to metaprogram how they're applied.
In the following example a constraint chooses an action based on a country code from the route data. The
full sample on GitHub.
You are responsible for implementing the Accept method and choosing an 'Order' for the constraint to
execute. In this case, the Accept method returns true to denote the action is a match when the country
route value matches. This is different from a RouteValueAttribute in that it allows fallback to a non-
attributed action. The sample shows that if you define an en-US action then a country code like fr-FR will
fall back to a more generic controller that doesn't have [CountrySpecific(...)] applied.
The Order property decides which stage the constraint is part of. Action constraints run in groups based on
the Order . For example, all of the framework provided HTTP method attributes use the same Order value
so that they run in the same stage. You can have as many stages as you need to implement your desired
policies.
TIP
To decide on a value for Order think about whether or not your constraint should be applied before HTTP methods.
Lower numbers run first.
File uploads in ASP.NET Core
6/21/2018 • 8 minutes to read • Edit Online
By Steve Smith
ASP.NET MVC actions support uploading of one or more files using simple model binding for smaller files or
streaming for larger files.
View or download sample from GitHub
In order to support file uploads, HTML forms must specify an enctype of multipart/form-data . The files input
element shown above supports uploading multiple files. Omit the multiple attribute on this input element to
allow just a single file to be uploaded. The above markup renders in a browser as:
The individual files uploaded to the server can be accessed through Model Binding using the IFormFile interface.
IFormFile has the following structure:
public interface IFormFile
{
string ContentType { get; }
string ContentDisposition { get; }
IHeaderDictionary Headers { get; }
long Length { get; }
string Name { get; }
string FileName { get; }
Stream OpenReadStream();
void CopyTo(Stream target);
Task CopyToAsync(Stream target, CancellationToken cancellationToken = null);
}
WARNING
Don't rely on or trust the FileName property without validation. The FileName property should only be used for display
purposes.
When uploading files using model binding and the IFormFile interface, the action method can accept either a
single IFormFile or an IEnumerable<IFormFile> (or List<IFormFile> ) representing several files. The following
example loops through one or more uploaded files, saves them to the local file system, and returns the total
number and size of files uploaded.
Warning: The following code uses GetTempFileName , which throws an IOException if more than 65535 files are
created without deleting previous temporary files. A real app should either delete temporary files or use
GetTempPath and GetRandomFileName to create temporary file names. The 65535 files limit is per server, so another
app on the server can use up all 65535 files.
[HttpPost("UploadFiles")]
public async Task<IActionResult> Post(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
Files uploaded using the IFormFile technique are buffered in memory or on disk on the web server before being
processed. Inside the action method, the IFormFile contents are accessible as a stream. In addition to the local file
system, files can be streamed to Azure Blob storage or Entity Framework.
To store binary file data in a database using Entity Framework, define a property of type byte[] on the entity:
public class ApplicationUser : IdentityUser
{
public byte[] AvatarImage { get; set; }
}
NOTE
IFormFile can be used directly as an action method parameter or as a viewmodel property, as shown above.
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser {
UserName = model.Email,
Email = model.Email
};
using (var memoryStream = new MemoryStream())
{
await model.AvatarImage.CopyToAsync(memoryStream);
user.AvatarImage = memoryStream.ToArray();
}
// additional logic omitted
NOTE
Use caution when storing binary data in relational databases, as it can adversely impact performance.
The following example demonstrates using JavaScript/Angular to stream to a controller action. The file's
antiforgery token is generated using a custom filter attribute and passed in HTTP headers instead of in the request
body. Because the action method processes the uploaded data directly, model binding is disabled by another filter.
Within the action, the form's contents are read using a MultipartReader , which reads each individual
MultipartSection , processing the file or storing the contents as appropriate. Once all sections have been read, the
action performs its own model binding.
The initial action loads the form and saves an antiforgery token in a cookie (via the
GenerateAntiforgeryTokenCookieForAjax attribute):
[HttpGet]
[GenerateAntiforgeryTokenCookieForAjax]
public IActionResult Index()
{
return View();
}
The attribute uses ASP.NET Core's built-in Antiforgery support to set a cookie with a request token:
Angular automatically passes an antiforgery token in a request header named X-XSRF-TOKEN . The ASP.NET Core
MVC app is configured to refer to this header in its configuration in Startup.cs:
services.AddMvc();
}
The DisableFormValueModelBinding attribute, shown below, is used to disable model binding for the Upload action
method.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
Since model binding is disabled, the Upload action method doesn't accept parameters. It works directly with the
Request property of ControllerBase . A MultipartReader is used to read each section. The file is saved with a
GUID filename and the key/value data is stored in a KeyValueAccumulator . Once all sections have been read, the
contents of the KeyValueAccumulator are used to bind the form data to a model type.
The complete Upload method is shown below:
Warning: The following code uses GetTempFileName , which throws an IOException if more than 65535 files are
created without deleting previous temporary files. A real app should either delete temporary files or use
GetTempPath and GetRandomFileName to create temporary file names. The 65535 files limit is per server, so another
app on the server can use up all 65535 files.
// 1. Disable the form value model binding here to take control of handling
// potentially large files.
// 2. Typically antiforgery tokens are sent in request body, but since we
// do not want to read the request body early, the tokens are made to be
// sent via headers. The antiforgery token filter first looks for tokens
// in the request header and then falls back to reading the body.
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
return BadRequest($"Expected a multipart request, but got {Request.ContentType}");
}
// Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator();
string targetFilePath = null;
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
{
targetFilePath = Path.GetTempFileName();
using (var targetStream = System.IO.File.Create(targetFilePath))
{
await section.Body.CopyToAsync(targetStream);
// Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
Troubleshooting
Below are some common problems encountered when working with uploading files and their possible solutions.
Unexpected Not Found error with IIS
The following error indicates your file upload exceeds the server's configured maxAllowedContentLength :
The default setting is 30000000 , which is approximately 28.6MB. The value can be customized by editing
web.config:
<system.webServer>
<security>
<requestFiltering>
<!-- This will handle requests up to 50MB -->
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
This setting only applies to IIS. The behavior doesn't occur by default when hosting on Kestrel. For more
information, see Request Limits <requestLimits>.
Null Reference Exception with IFormFile
If your controller is accepting uploaded files using IFormFile but you find that the value is always null, confirm
that your HTML form is specifying an enctype value of multipart/form-data . If this attribute isn't set on the
<form> element, the file upload won't occur and any bound IFormFile arguments will be null.
Dependency injection into controllers in ASP.NET
Core
7/30/2018 • 5 minutes to read • Edit Online
By Steve Smith
ASP.NET Core MVC controllers should request their dependencies explicitly via their constructors. In some
instances, individual controller actions may require a service, and it may not make sense to request at the
controller level. In this case, you can also choose to inject a service as a parameter on the action method.
View or download sample code (how to download)
Dependency Injection
Dependency injection is a technique that follows the Dependency Inversion Principle, allowing for applications to
be composed of loosely coupled modules. ASP.NET Core has built-in support for dependency injection, which
makes applications easier to test and maintain.
Constructor Injection
ASP.NET Core's built-in support for constructor-based dependency injection extends to MVC controllers. By
simply adding a service type to your controller as a constructor parameter, ASP.NET Core will attempt to resolve
that type using its built in service container. Services are typically, but not always, defined using interfaces. For
example, if your application has business logic that depends on the current time, you can inject a service that
retrieves the time (rather than hard-coding it), which would allow your tests to pass in implementations that use a
set time.
using System;
namespace ControllerDI.Interfaces
{
public interface IDateTime
{
DateTime Now { get; }
}
}
Implementing an interface like this one so that it uses the system clock at runtime is trivial:
using System;
using ControllerDI.Interfaces;
namespace ControllerDI.Services
{
public class SystemDateTime : IDateTime
{
public DateTime Now
{
get { return DateTime.Now; }
}
}
}
With this in place, we can use the service in our controller. In this case, we have added some logic to the
HomeController Index method to display a greeting to the user based on the time of day.
using ControllerDI.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ControllerDI.Controllers
{
public class HomeController : Controller
{
private readonly IDateTime _dateTime;
This error occurs when we have not configured a service in the ConfigureServices method in our Startup class.
To specify that requests for IDateTime should be resolved using an instance of SystemDateTime , add the
highlighted line in the listing below to your ConfigureServices method:
Once the service has been configured, running the application and navigating to the home page should display
the time-based message as expected:
TIP
See Test controller logic to learn how to explicitly request dependencies http://deviq.com/explicit-dependencies-principle/ in
controllers makes code easier to test.
ASP.NET Core's built-in dependency injection supports having only a single constructor for classes requesting
services. If you have more than one constructor, you may get an exception stating:
InvalidOperationException: Multiple constructors accepting all given argument types have been found in type
'ControllerDI.Controllers.HomeController'. There should only be one applicable constructor.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType,
Type[] argumentTypes, ConstructorInfo& matchingConstructor, Nullable`1[]& parameterMap)
As the error message states, you can correct this problem with the use of a single constructor. You can also
replace the default dependency injection container with a third party implementation, many of which support
multiple constructors.
return View();
}
namespace ControllerDI.Model
{
public class SampleWebSettings
{
public string Title { get; set; }
public int Updates { get; set; }
}
}
Then you need to configure the application to use the options model and add your configuration class to the
services collection in ConfigureServices :
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?
LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// Required to use the Options<T> pattern
services.AddOptions();
services.AddMvc();
NOTE
In the above listing, we are configuring the application to read the settings from a JSON-formatted file. You can also
configure the settings entirely in code, as is shown in the commented code above. See Configuration for further
configuration options.
Once you've specified a strongly-typed configuration object (in this case, SampleWebSettings ) and added it to the
services collection, you can request it from any Controller or Action method by requesting an instance of
IOptions<T> (in this case, IOptions<SampleWebSettings> ). The following code shows how one would request the
settings from a controller:
public class SettingsController : Controller
{
private readonly SampleWebSettings _settings;
Following the Options pattern allows settings and configuration to be decoupled from one another, and ensures
the controller is following separation of concerns, since it doesn't need to know how or where to find the settings
information. It also makes the controller easier to unit test Test controller logic, since there's no static cling or
direct instantiation of settings classes within the controller class.
Test controller logic in ASP.NET Core
9/18/2018 • 12 minutes to read • Edit Online
By Steve Smith
Controllers play a central role in any ASP.NET Core MVC app. As such, you should have confidence that controllers
behave as intended. Automated tests can detect errors before the app is deployed to a production environment.
View or download sample code (how to download)
return View(model);
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
The Home controller's HTTP POST Index method tests verifies that:
When ModelState.IsValid is false , the action method returns a 400 Bad Request ViewResult with the
appropriate data.
When ModelState.IsValid is true :
The Add method on the repository is called.
A RedirectToActionResult is returned with the correct arguments.
An invalid model state is tested by adding errors using AddModelError as shown in the first test below:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
When ModelState isn't valid, the same ViewResult is returned as for a GET request. The test doesn't attempt to
pass in an invalid model. Passing an invalid model isn't a valid approach, since model binding isn't running
(although an integration test does use model binding). In this case, model binding isn't tested. These unit tests are
only testing the code in the action method.
The second test verifies that when the ModelState is valid:
A new BrainstormSession is added (via the repository).
The method returns a RedirectToActionResult with the expected properties.
Mocked calls that aren't called are normally ignored, but calling Verifiable at the end of the setup call allows mock
validation in the test. This is performed with the call to mockRepo.Verify , which fails the test if the expected method
wasn't called.
NOTE
The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called
"loose" mocks or stubs). Learn more about customizing Mock behavior with Moq.
Another controller in the sample app displays information related to a particular brainstorming session. The
controller includes logic to deal with invalid id values (there are two return scenarios in the following example to
cover these scenarios). The final return statement returns a new StormSessionViewModel to the view:
return View(viewModel);
}
}
The unit tests include one test for each return scenario in the Session controller Index action:
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}
Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:
A list of ideas ( IdeaDTO ) associated with a brainstorming session is returned by the ForSession method.
The Create method adds new ideas to a session.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Avoid returning business domain entities directly via API calls. Domain entities:
Often include more data than the client requires.
Unnecessarily couple the app's internal domain model with the publicly exposed API.
Mapping between domain entities and the types returned to the client can be performed:
Manually with a LINQ Select , as the sample app uses. For more information, see LINQ (Language Integrated
Query).
Automatically with a library, such as AutoMapper.
Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas controller.
The sample app contains two ForSession tests. The first test determines if ForSession returns a
NotFoundObjectResult (HTTP Not Found) for an invalid session:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
The second ForSession test determines if ForSession returns a list of session ideas ( <List<IdeaDTO>> ) for a valid
session. The checks also examine the first idea to confirm its Name property is correct:
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
To test the behavior of the Create method when the ModelState is invalid, the sample app adds a model error to
the controller as part of the test. Don't try to test model validation or model binding in unit tests—just test the
action method's behavior when confronted with an invalid ModelState :
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.Create(model: null);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
The second test of Create depends on the repository returning null , so the mock repository is configured to
return null . There's no need to create a test database (in memory or otherwise) and construct a query that returns
this result. The test can be accomplished in a single statement, as the sample code illustrates:
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
The third Create test, Create_ReturnsNewlyCreatedIdeaForSession , verifies that the repository's UpdateAsync
method is called. The mock is called with Verifiable , and the mocked repository's Verify method is called to
confirm the verifiable method is executed. It's not the unit test's responsibility to ensure that the UpdateAsync
method saved the data—that can be performed with an integration test.
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(newIdea);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Test ActionResult<T>
In ASP.NET Core 2.1 or later, ActionResult<T> (ActionResult<TValue>) enables you to return a type deriving from
ActionResult or return a specific type.
The sample app includes a method that returns a List<IdeaDTO> for a given session id . If the session id doesn't
exist, the controller returns NotFound:
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
return result;
}
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;
// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
For for a valid session id , the second test confirms that the method returns:
An ActionResult with a List<IdeaDTO> type.
The ActionResult<T>.Value is a List<IdeaDTO> type.
The first item in the list is a valid idea matching the idea stored in the mock session (obtained by calling
GetTestSession ).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSessionActionResult(testSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
The sample app also includes a method to create a new Idea for a given session. The controller returns:
BadRequest for an invalid model.
NotFound if the session doesn't exist.
CreatedAtAction when the session is updated with the new idea.
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (session == null)
{
return NotFound(model.SessionId);
}
await _sessionRepository.UpdateAsync(session);
// Act
var result = await controller.CreateActionResult(model: null);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
The second test checks that a NotFound is returned if the session doesn't exist.
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Additional resources
Test, debug, and troubleshoot in ASP.NET Core
Integration tests in ASP.NET Core
Create and run unit tests with Visual Studio.
Repository pattern with ASP.NET Core
Explicit Dependencies Principle
Advanced topics for ASP.NET Core MVC
7/10/2018 • 2 minutes to read • Edit Online
By Steve Smith
ASP.NET Core MVC defines an application model representing the components of an MVC app. You can read and
manipulate this model to modify how MVC elements behave. By default, MVC follows certain conventions to
determine which classes are considered to be controllers, which methods on those classes are actions, and how
parameters and routing behave. You can customize this behavior to suit your app's needs by creating your own
conventions and applying them globally or as attributes.
NOTE
The ActionDescriptor.Properties collection isn't thread safe (for writes) once app startup has finished. Conventions are
the best way to safely add data to this collection.
IApplicationModelProvider
ASP.NET Core MVC loads the application model using a provider pattern, defined by the
IApplicationModelProvider interface. This section covers some of the internal implementation details of how this
provider functions. This is an advanced topic - most apps that leverage the application model should do so by
working with conventions.
Implementations of the IApplicationModelProvider interface "wrap" one another, with each implementation calling
OnProvidersExecuting in ascending order based on its Order property. The OnProvidersExecuted method is then
called in reverse order. The framework defines several providers:
First ( Order=-1000 ):
DefaultApplicationModelProvider
Then ( Order=-990 ):
AuthorizationApplicationModelProvider
CorsApplicationModelProvider
NOTE
The order in which two providers with the same value for Order are called is undefined, and therefore shouldn't be relied
upon.
NOTE
IApplicationModelProvider is an advanced concept for framework authors to extend. In general, apps should use
conventions and frameworks should use providers. The key distinction is that providers always run before conventions.
The DefaultApplicationModelProvider establishes many of the default behaviors used by ASP.NET Core MVC. Its
responsibilities include:
Adding global filters to the context
Adding controllers to the context
Adding public controller methods as actions
Adding action method parameters to the context
Applying route and other attributes
Some built-in behaviors are implemented by the DefaultApplicationModelProvider . This provider is responsible for
constructing the ControllerModel , which in turn references ActionModel , PropertyModel , and ParameterModel
instances. The DefaultApplicationModelProvider class is an internal framework implementation detail that can and
will change in the future.
The AuthorizationApplicationModelProvider is responsible for applying the behavior associated with the
AuthorizeFilter and AllowAnonymousFilter attributes. Learn more about these attributes.
The implements behavior associated with the IEnableCorsAttribute and
CorsApplicationModelProvider
IDisableCorsAttribute , and the DisableCorsAuthorizationFilter . Learn more about CORS.
Conventions
The application model defines convention abstractions that provide a simpler way to customize the behavior of the
models than overriding the entire model or provider. These abstractions are the recommended way to modify your
app's behavior. Conventions provide a way for you to write code that will dynamically apply customizations. While
filters provide a means of modifying the framework's behavior, customizations let you control how the whole app
is wired together.
The following conventions are available:
IApplicationModelConvention
IControllerModelConvention
IActionModelConvention
IParameterModelConvention
Conventions are applied by adding them to MVC options or by implementing Attribute s and applying them to
controllers, actions, or action parameters (similar to Filters ). Unlike filters, conventions are only executed when
the app is starting, not as part of each request.
Sample: Modifying the ApplicationModel
The following convention is used to add a property to the application model.
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class ApplicationDescription : IApplicationModelConvention
{
private readonly string _description;
Application model conventions are applied as options when MVC is added in ConfigureServices in Startup .
Properties are accessible from the ActionDescriptor properties collection within controller actions:
namespace AppModelSample.Conventions
{
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
{
private readonly string _description;
[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class ActionDescriptionAttribute : Attribute, IActionModelConvention
{
private readonly string _description;
Applying this to an action within the previous example's controller demonstrates how it overrides the controller-
level convention:
[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
public string Index()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}
[ActionDescription("Action Description")]
public string UseActionDescriptionAttribute()
{
return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
}
}
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace AppModelSample.Conventions
{
public class MustBeInRouteParameterModelConvention : Attribute, IParameterModelConvention
{
public void Apply(ParameterModel model)
{
if (model.BindingInfo == null)
{
model.BindingInfo = new BindingInfo();
}
model.BindingInfo.BindingSource = BindingSource.Path;
}
}
}
namespace AppModelSample.Conventions
{
public class CustomActionNameAttribute : Attribute, IActionModelConvention
{
private readonly string _actionName;
// Route: /Home/MyCoolAction
[CustomActionName("MyCoolAction")]
public string SomeName()
{
return ControllerContext.ActionDescriptor.ActionName;
}
Even though the method name is SomeName , the attribute overrides the MVC convention of using the method
name and replaces the action name with MyCoolAction . Thus, the route used to reach this action is
/Home/MyCoolAction .
NOTE
This example is essentially the same as using the built-in ActionName attribute.
namespace AppModelSample.Conventions
{
public class NamespaceRoutingConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var hasAttributeRouteModels = controller.Selectors
.Any(selector => selector.AttributeRouteModel != null);
if (!hasAttributeRouteModels
&& controller.ControllerName.Contains("Namespace")) // affect one controller in this
sample
{
// Replace the . in the namespace with a / to create the attribute route
// Ex: MySite.Admin namespace will correspond to MySite/Admin attribute route
// Then attach [controller], [action] and optional {id?} token.
// [Controller] and [action] is replaced with the controller and action
// name to generate the final template
controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
{
Template = controller.ControllerType.Namespace.Replace('.', '/') +
"/[controller]/[action]/{id?}"
};
}
}
// You can continue to put attribute route templates for the controller actions depending on the
way you want them to behave
}
}
}
TIP
You can add conventions to your middleware by accessing MvcOptions using
services.Configure<MvcOptions>(c => c.Conventions.Add(YOURCONVENTION));
This sample applies this convention to routes that are not using attribute routing where the controller has
"Namespace" in its name. The following controller demonstrates this convention:
using Microsoft.AspNetCore.Mvc;
namespace AppModelSample.Controllers
{
public class NamespaceRoutingController : Controller
{
// using NamespaceRoutingConvention
// route: /AppModelSample/Controllers/NamespaceRouting/Index
public string Index()
{
return "This demonstrates namespace routing.";
}
}
}
NOTE
Learn more about migration from ASP.NET Web API.
To use the Web API Compatibility Shim, you need to add the package to your project and then add the
conventions to MVC by calling AddWebApiConventions in Startup :
services.AddMvc().AddWebApiConventions();
The conventions provided by the shim are only applied to parts of the app that have had certain attributes applied
to them. The following four attributes are used to control which controllers should have their conventions modified
by the shim's conventions:
UseWebApiActionConventionsAttribute
UseWebApiOverloadingAttribute
UseWebApiParameterConventionsAttribute
UseWebApiRoutesAttribute
Action Conventions
The UseWebApiActionConventionsAttribute is used to map the HTTP method to actions based on their name (for
instance, Get would map to HttpGet ). It only applies to actions that don't use attribute routing.
Overloading
The UseWebApiOverloadingAttribute is used to apply the WebApiOverloadingApplicationModelConvention convention.
This convention adds an OverloadActionConstraint to the action selection process, which limits candidate actions
to those for which the request satisfies all non-optional parameters.
Parameter Conventions
The UseWebApiParameterConventionsAttribute is used to apply the
WebApiParameterConventionsApplicationModelConvention action convention. This convention specifies that simple
types used as action parameters are bound from the URI by default, while complex types are bound from the
request body.
Routes
The UseWebApiRoutesAttribute controls whether the WebApiApplicationModelConvention controller convention is
applied. When enabled, this convention is used to add support for areas to the route.
In addition to a set of conventions, the compatibility package includes a System.Web.Http.ApiController base class
that replaces the one provided by Web API. This allows your controllers written for Web API and inheriting from
its ApiController to work as they were designed, while running on ASP.NET Core MVC. This base controller class
is decorated with all of the UseWebApi* attributes listed above. The ApiController exposes properties, methods,
and result types that are compatible with those found in Web API.
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace AppModelSample.Conventions
{
public class EnableApiExplorerApplicationConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
application.ApiExplorer.IsVisible = true;
}
}
}
Using this approach (and additional conventions if required), you can enable or disable API visibility at any level
within your app.
Filters in ASP.NET Core
9/27/2018 • 19 minutes to read • Edit Online
IMPORTANT
This topic does not apply to Razor Pages. ASP.NET Core 2.1 and later supports IPageFilter and IAsyncPageFilter for Razor
Pages. For more information, see Filter methods for Razor Pages.
using FiltersSample.Helper;
using Microsoft.AspNetCore.Mvc.Filters;
namespace FiltersSample.Filters
{
public class SampleActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
}
namespace FiltersSample.Filters
{
public class SampleAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// do something before the action executes
var resultContext = await next();
// do something after the action executes; resultContext.Result will be set
}
}
}
You can implement interfaces for multiple filter stages in a single class. For example, the ActionFilterAttribute
class implements IActionFilter , IResultFilter , and their async equivalents.
NOTE
Implement either the synchronous or the async version of a filter interface, not both. The framework checks first to see if
the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's method(s). If you
were to implement both interfaces on one class, only the async method would be called. When using abstract classes like
ActionFilterAttribute you would override only the synchronous methods or the async method for each filter type.
IFilterFactory
IFilterFactory implements IFilterMetadata. Therefore, an IFilterFactory instance can be used as an
IFilterMetadata instance anywhere in the filter pipeline. When the framework prepares to invoke the filter, it
attempts to cast it to an IFilterFactory . If that cast succeeds, the CreateInstance method is called to create the
IFilterMetadata instance that will be invoked. This provides a flexible design, since the precise filter pipeline
doesn't need to be set explicitly when the app starts.
You can implement IFilterFactory on your own attribute implementations as another approach to creating
filters:
public class AddHeaderWithFactoryAttribute : Attribute, IFilterFactory
{
// Implement IFilterFactory
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new InternalAddHeaderFilter();
}
using Microsoft.AspNetCore.Mvc.Filters;
namespace FiltersSample.Filters
{
public class AddHeaderAttribute : ResultFilterAttribute
{
private readonly string _name;
private readonly string _value;
Attributes allow filters to accept arguments, as shown in the example above. You would add this attribute to a
controller or action method and specify the name and value of the HTTP header:
[AddHeader("Author", "Steve Smith @ardalis")]
public class SampleController : Controller
{
public IActionResult Index()
{
return Content("Examine the headers using developer tools.");
}
[ShortCircuitingResourceFilter]
public IActionResult SomeResource()
{
return Content("Successful access to resource - header should be set.");
}
The result of the Index action is shown below - the response headers are displayed on the bottom right.
Several of the filter interfaces have corresponding attributes that can be used as base classes for custom
implementations.
Filter attributes:
ActionFilterAttribute
ExceptionFilterAttribute
ResultFilterAttribute
FormatFilterAttribute
ServiceFilterAttribute
TypeFilterAttribute
services.AddScoped<AddHeaderFilterWithDi>();
}
1 Global OnActionExecuting
2 Controller OnActionExecuting
3 Method OnActionExecuting
4 Method OnActionExecuted
5 Controller OnActionExecuted
6 Global OnActionExecuted
If you have the same 3 Action filters shown in the preceding example but set the Order property of the
controller and global filters to 1 and 2 respectively, the order of execution would be reversed.
1 Method 0 OnActionExecuting
2 Controller 1 OnActionExecuting
3 Global 2 OnActionExecuting
4 Global 2 OnActionExecuted
5 Controller 1 OnActionExecuted
6 Method 0 OnActionExecuted
The Order property trumps scope when determining the order in which filters will run. Filters are sorted first
by order, then scope is used to break ties. All of the built-in filters implement IOrderedFilter and set the
default Order value to 0. For built-in filters, scope determines order unless you set Order to a non-zero value.
namespace FiltersSample.Filters
{
public class ShortCircuitingResourceFilterAttribute : Attribute,
IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.Result = new ContentResult()
{
Content = "Resource unavailable - header should not be set"
};
}
In the following code, both the ShortCircuitingResourceFilter and the AddHeader filter target the
SomeResource action method. The ShortCircuitingResourceFilter :
Runs first, because it's a Resource Filter and AddHeader is an Action Filter.
Short-circuits the rest of the pipeline.
Therefore the AddHeader filter never runs for the SomeResource action. This behavior would be the same if both
filters were applied at the action method level, provided the ShortCircuitingResourceFilter ran first. The
ShortCircuitingResourceFilter runs first because of its filter type, or by explicit use of Order property.
[ShortCircuitingResourceFilter]
public IActionResult SomeResource()
{
return Content("Successful access to resource - header should be set.");
}
Dependency injection
Filters can be added by type or by instance. If you add an instance, that instance will be used for every request.
If you add a type, it will be type-activated, meaning an instance will be created for each request and any
constructor dependencies will be populated by dependency injection (DI). Adding a filter by type is equivalent
to filters.Add(new TypeFilterAttribute(typeof(MyFilter))) .
Filters that are implemented as attributes and added directly to controller classes or action methods cannot
have constructor dependencies provided by dependency injection (DI). This is because attributes must have
their constructor parameters supplied where they're applied. This is a limitation of how attributes work.
If your filters have dependencies that you need to access from DI, there are several supported approaches. You
can apply your filter to a class or action method using one of the following:
ServiceFilterAttribute
TypeFilterAttribute
IFilterFactory implemented on your attribute
NOTE
One dependency you might want to get from DI is a logger. However, avoid creating and using filters purely for logging
purposes, since the built-in framework logging features may already provide what you need. If you're going to add
logging to your filters, it should focus on business domain concerns or behavior specific to your filter, rather than MVC
actions or other framework events.
ServiceFilterAttribute
A ServiceFilter retrieves an instance of the filter from DI. You add the filter to the container in
ConfigureServices , and reference it in a ServiceFilter attribute
services.AddScoped<AddHeaderFilterWithDi>();
}
[ServiceFilter(typeof(AddHeaderFilterWithDi))]
public IActionResult Index()
{
return View();
}
The following example demonstrates how to pass arguments to a type using TypeFilterAttribute :
[TypeFilter(typeof(AddHeaderAttribute),
Arguments = new object[] { "Author", "Steve Smith (@ardalis)" })]
public IActionResult Hi(string name)
{
return Content($"Hi {name}");
}
This filter can be applied to classes or methods using the [SampleActionFilter] syntax, instead of having to use
[TypeFilter] or [ServiceFilter] .
Authorization filters
*Authorization filters:
Control access to action methods.
Are the first filters to be executed within the filter pipeline.
Have a before method, but no after method.
You should only write a custom authorization filter if you are writing your own authorization framework. Prefer
configuring your authorization policies or writing a custom authorization policy over writing a custom filter.
The built-in filter implementation is just responsible for calling the authorization system.
You shouldn't throw exceptions within authorization filters, since nothing will handle the exception (exception
filters won't handle them). Consider issuing a challenge when an exception occurs.
Learn more about Authorization.
Resource filters
Implement either the IResourceFilter or IAsyncResourceFilter interface,
Their execution wraps most of the filter pipeline.
Only Authorization filters run before Resource filters.
Resource filters are useful to short-circuit most of the work a request is doing. For example, a caching filter can
avoid the rest of the pipeline if the response is in the cache.
The short circuiting resource filter shown earlier is one example of a resource filter. Another example is
DisableFormValueModelBindingAttribute:
It prevents model binding from accessing the form data.
It's useful for large file uploads and want to prevent the form from being read into memory.
Action filters
Action filters:
Implement either the IActionFilter or IAsyncActionFilter interface.
Their execution surrounds the execution of action methods.
Here's a sample action filter:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace FiltersSample.Filters
{
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
}
The OnActionExecuted method runs after the action method and can see and manipulate the results of the
action through the ActionExecutedContext.Result property. ActionExecutedContext.Canceled will be set to true
if the action execution was short-circuited by another filter. ActionExecutedContext.Exception will be set to a
non-null value if the action or a subsequent action filter threw an exception. Setting
ActionExecutedContext.Exception to null:
Exception filters
Exception filters implement either the IExceptionFilter or IAsyncExceptionFilter interface. They can be used
to implement common error handling policies for an app.
The following sample exception filter uses a custom developer error view to display details about exceptions
that occur when the app is in development:
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilterAttribute(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
Exception filters:
Don't have before and after events.
Implement OnException or OnExceptionAsync .
Handle unhandled exceptions that occur in controller creation, model binding, action filters, or action
methods.
Do not catch exceptions that occur in Resource filters, Result filters, or MVC Result execution.
To handle an exception, set the ExceptionContext.ExceptionHandled property to true or write a response. This
stops propagation of the exception. An Exception filter can't turn an exception into a "success". Only an Action
filter can do that.
NOTE
In ASP.NET Core 1.1, the response isn't sent if you set ExceptionHandled to true and write a response. In that
scenario, ASP.NET Core 1.0 does send the response, and ASP.NET Core 1.1.2 will return to the 1.0 behavior. For more
information, see issue #5594 in the GitHub repository.
Exception filters:
Are good for trapping exceptions that occur within MVC actions.
Are not as flexible as error handling middleware.
Prefer middleware for exception handling. Use exception filters only where you need to do error handling
differently based on which MVC action was chosen. For example, your app might have action methods for both
API endpoints and for views/HTML. The API endpoints could return error information as JSON, while the
view -based actions could return an error page as HTML.
The ExceptionFilterAttribute can be subclassed.
Result filters
Implement either the IResultFilter or IAsyncResultFilter interface.
Their execution surrounds the execution of action results.
Here's an example of a Result filter that adds an HTTP header.
The kind of result being executed depends on the action in question. An MVC action returning a view would
include all razor processing as part of the ViewResult being executed. An API method might perform some
serialization as part of the execution of the result. Learn more about action results
Result filters are only executed for successful results - when the action or action filters produce an action result.
Result filters are not executed when exception filters handle an exception.
The OnResultExecuting method can short-circuit execution of the action result and subsequent result filters by
setting ResultExecutingContext.Cancel to true. You should generally write to the response object when short-
circuiting to avoid generating an empty response. Throwing an exception will:
Prevent execution of the action result and subsequent filters.
Be treated as a failure instead of a successful result.
When the OnResultExecuted method runs, the response has likely been sent to the client and cannot be
changed further (unless an exception was thrown). ResultExecutedContext.Canceled will be set to true if the
action result execution was short-circuited by another filter.
ResultExecutedContext.Exception will be set to a non-null value if the action result or a subsequent result filter
threw an exception. Setting Exception to null effectively 'handles' an exception and prevents the exception
from being rethrown by MVC later in the pipeline. When you're handling an exception in a result filter, you
might not be able to write any data to the response. If the action result throws partway through its execution,
and the headers have already been flushed to the client, there's no reliable mechanism to send a failure code.
For an IAsyncResultFilter a call to await next on the ResultExecutionDelegate executes any subsequent
result filters and the action result. To short-circuit, set ResultExecutingContext.Cancel to true and don't call the
ResultExectionDelegate .
The framework provides an abstract ResultFilterAttribute that you can subclass. The AddHeaderAttribute
class shown earlier is an example of a result filter attribute.
Using middleware in the filter pipeline
Resource filters work like middleware in that they surround the execution of everything that comes later in the
pipeline. But filters differ from middleware in that they're part of MVC, which means that they have access to
MVC context and constructs.
In ASP.NET Core 1.1, you can use middleware in the filter pipeline. You might want to do that if you have a
middleware component that needs access to MVC route data, or one that should run only for certain
controllers or actions.
To use middleware as a filter, create a type with a Configure method that specifies the middleware that you
want to inject into the filter pipeline. Here's an example that uses the localization middleware to establish the
current culture for a request:
applicationBuilder.UseRequestLocalization(options);
}
}
You can then use the MiddlewareFilterAttribute to run the middleware for a selected controller or action or
globally:
[Route("{culture}/[controller]/[action]")]
[MiddlewareFilter(typeof(LocalizationPipeline))]
public IActionResult CultureFromRouteData()
{
return Content($"CurrentCulture:{CultureInfo.CurrentCulture.Name},"
+ $"CurrentUICulture:{CultureInfo.CurrentUICulture.Name}");
}
Middleware filters run at the same stage of the filter pipeline as Resource filters, before model binding and
after the rest of the pipeline.
Next actions
To experiment with filters, download, test and modify the sample.
Areas in ASP.NET Core
8/30/2018 • 4 minutes to read • Edit Online
/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
These are the default locations which can be changed via the AreaViewLocationFormats on the
Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions .
For example, in the below code instead of having the folder name as 'Areas', it has been changed to 'Categories'.
services.Configure<RazorViewEngineOptions>(options =>
{
options.AreaViewLocationFormats.Clear();
options.AreaViewLocationFormats.Add("/Categories/{2}/Views/{1}/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Categories/{2}/Views/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});
One thing to note is that the structure of the Views folder is the only one which is considered important here and
the content of the rest of the folders like Controllers and Models does not matter. For example, you need not
have a Controllers and Models folder at all. This works because the content of Controllers and Models is just code
which gets compiled into a .dll where as the content of the Views isn't until a request to that view has been made.
Once you've defined the folder hierarchy, you need to tell MVC that each controller is associated with an area.
You do that by decorating the controller name with the [Area] attribute.
...
namespace MyStore.Areas.Products.Controllers
{
[Area("Products")]
public class HomeController : Controller
{
// GET: /Products/Home/Index
public IActionResult Index()
{
return View();
}
// GET: /Products/Home/Create
public IActionResult Create()
{
return View();
}
}
}
Set up a route definition that works with your newly created areas. The Route to controller actions article goes
into detail about how to create route definitions, including using conventional routes versus attribute routes. In
this example, we'll use a conventional route. To do so, open the Startup.cs file and modify it by adding the
areaRoute named route definition below.
...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Browsing to http://<yourApp>/products , the Index action method of the HomeController in the Products area
will be invoked.
Link Generation
Generating links from an action within an area based controller to another action within the same
controller.
Let's say the current request's path is like /Products/Home/Create
Note that we need not supply the 'area' and 'controller' values here as they're already available in the
context of the current request. These kind of values are called ambient values.
Generating links from an action within an area based controller to another action on a different controller
Let's say the current request's path is like /Products/Home/Create
HtmlHelper syntax:
@Html.ActionLink("Go to Services Home Page", "Index", "Home", new { area = "Services" })
TagHelper syntax:
<a asp-area="Services" asp-controller="Home" asp-action="Index">Go to Services Home Page</a>
TagHelper syntax:
<a asp-area="" asp-controller="Manage" asp-action="Index">Go to Manage Products Home Page</a>
Since we want to generate links to a non-area based controller action, we empty the ambient value for
'area' here.
Publishing Areas
All *.cshtml and wwwroot/** files are published to output when <Project Sdk="Microsoft.NET.Sdk.Web"> is
included in the .csproj file.
Application Parts in ASP.NET Core
9/18/2018 • 4 minutes to read • Edit Online
// OR
var assembly = typeof(Startup).GetTypeInfo().Assembly;
var part = new AssemblyPart(assembly);
services.AddMvc()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
By default MVC will search the dependency tree and find controllers (even in other assemblies). To load an
arbitrary assembly (for instance, from a plugin that isn't referenced at compile time), you can use an application
part.
You can use application parts to avoid looking for controllers in a particular assembly or location. You can control
which parts (or assemblies) are available to the app by modifying the ApplicationParts collection of the
ApplicationPartManager . The order of the entries in the ApplicationParts collection isn't important. It's important
to fully configure the ApplicationPartManager before using it to configure services in the container. For example,
you should fully configure the ApplicationPartManager before invoking AddControllersAsServices . Failing to do so,
will mean that controllers in application parts added after that method call won't be affected (won't get registered
as services) which might result in incorrect behavior of your application.
If you have an assembly that contains controllers you don't want to be used, remove it from the
ApplicationPartManager :
services.AddMvc()
.ConfigureApplicationPartManager(apm =>
{
var dependentLibrary = apm.ApplicationParts
.FirstOrDefault(part => part.Name == "DependentLibrary");
if (dependentLibrary != null)
{
apm.ApplicationParts.Remove(dependentLibrary);
}
})
In addition to your project's assembly and its dependent assemblies, the ApplicationPartManager will include parts
for Microsoft.AspNetCore.Mvc.TagHelpers and Microsoft.AspNetCore.Mvc.Razor by default.
services.AddMvc()
.ConfigureApplicationPartManager(apm =>
apm.FeatureProviders.Add(new GenericControllerFeatureProvider()));
By default, the generic controller names used for routing would be of the form GenericController`1 [Widget]
instead of Widget. The following attribute is used to modify the name to correspond to the generic type used by
the controller:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System;
namespace AppPartsSample
{
// Used to set the controller name for routing purposes. Without this convention the
// names would be like 'GenericController`1[Widget]' instead of 'Widget'.
//
// Conventions can be applied as attributes or added to MvcOptions.Conventions.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class GenericControllerNameConvention : Attribute, IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.GetGenericTypeDefinition() !=
typeof(GenericController<>))
{
// Not a GenericController, ignore.
return;
}
namespace AppPartsSample
{
[GenericControllerNameConvention] // Sets the controller name based on typeof(T).Name
public class GenericController<T> : Controller
{
public IActionResult Index()
{
return Content($"Hello from a generic {typeof(T).Name} controller.");
}
}
}
namespace AppPartsSample.Controllers
{
public class FeaturesController : Controller
{
private readonly ApplicationPartManager _partManager;
return View(viewModel);
}
}
}
Example output:
Custom Model Binding in ASP.NET Core
6/21/2018 • 6 minutes to read • Edit Online
By Steve Smith
Model binding allows controller actions to work directly with model types (passed in as method arguments), rather
than HTTP requests. Mapping between incoming request data and application models is handled by model
binders. Developers can extend the built-in model binding functionality by implementing custom model binders
(though typically, you don't need to write your own provider).
View or download sample from GitHub
Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented.
Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. The
byte arrays are often stored as files or database BLOB fields.
Working with the ByteArrayModelBinder
Base64-encoded strings can be used to represent binary data. For example, the following image can be encoded as
a string.
if (context.Metadata.ModelType == typeof(byte[]))
{
return new ByteArrayModelBinder();
}
return null;
}
When creating your own custom model binder, you can implement your own IModelBinderProvider type, or use
the ModelBinderAttribute.
The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[]
and save the result to a file:
// POST: api/image
[HttpPost]
public void Post(byte[] file, string filename)
{
string filePath = Path.Combine(_env.ContentRootPath, "wwwroot/images/upload", filename);
if (System.IO.File.Exists(filePath)) return;
System.IO.File.WriteAllBytes(filePath, file);
}
You can POST a base64-encoded string to this api method using a tool like Postman:
As long as the binder can bind request data to appropriately named properties or arguments, model binding will
succeed. The following example shows how to use ByteArrayModelBinder with a view model:
[HttpPost("Profile")]
public void SaveProfile(ProfileViewModel model)
{
string filePath = Path.Combine(_env.ContentRootPath, "wwwroot/images/upload", model.FileName);
if (System.IO.File.Exists(model.FileName)) return;
System.IO.File.WriteAllBytes(filePath, model.File);
}
using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CustomModelBindingSample.Data
{
[ModelBinder(BinderType = typeof(AuthorEntityBinder))]
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public string GitHub { get; set; }
public string Twitter { get; set; }
public string BlogUrl { get; set; }
}
}
In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind
Author action parameters.
The AuthorEntityBinder is used to bind an Author parameter by fetching the entity from a data source using
Entity Framework Core and an authorId :
public class AuthorEntityBinder : IModelBinder
{
private readonly AppDbContext _db;
public AuthorEntityBinder(AppDbContext db)
{
_db = db;
}
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName,
valueProviderResult);
int id = 0;
if (!int.TryParse(value, out id))
{
// Non-integer arguments result in model state errors
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
"Author Id must be an integer.");
return Task.CompletedTask;
}
The following code shows how to use the AuthorEntityBinder in an action method:
[HttpGet("get/{authorId}")]
public IActionResult Get(Author author)
{
return Ok(author);
}
The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default
conventions:
[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")]Author author)
{
if (author == null)
{
return NotFound();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok(author);
}
In this example, since the name of the argument isn't the default authorId , it's specified on the parameter using
ModelBinder attribute. Note that both the controller and action method are simplified compared to looking up the
entity in the action method. The logic to fetch the author using Entity Framework Core is moved to the model
binder. This can be considerable simplification when you have several methods that bind to the author model, and
can help you to follow the DRY principle.
You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action
method parameters to specify a certain model binder or model name for just that type or action.
Implementing a ModelBinderProvider
Instead of applying an attribute, you can implement IModelBinderProvider . This is how the built-in framework
binders are implemented. When you specify the type your binder operates on, you specify the type of argument it
produces, not the input your binder accepts. The following binder provider works with the AuthorEntityBinder .
When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or
Author typed parameters.
using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;
namespace CustomModelBindingSample.Binders
{
public class AuthorEntityBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(Author))
{
return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
}
return null;
}
}
}
Note: The preceding code returns a BinderTypeModelBinder . BinderTypeModelBinder acts as a factory for model
binders and provides dependency injection (DI). The AuthorEntityBinder requires DI to access EF Core. Use
BinderTypeModelBinder if your model binder requires services from DI.
services.AddMvc(options =>
{
// add custom binder to beginning of collection
options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
});
}
When evaluating model binders, the collection of providers is examined in order. The first provider that returns a
binder is used.
The following image shows the default model binders from the debugger.
Adding your provider to the end of the collection may result in a built-in model binder being called before your
custom binder has a chance. In this example, the custom provider is added to the beginning of the collection to
ensure it's used for Author action arguments.
services.AddMvc(options =>
{
// add custom binder to beginning of collection
options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
});
}
By Rick Anderson
The SetCompatibilityVersion method allows an app to opt-in or opt-out of potentially breaking behavior changes
introduced in ASP.NET Core MVC 2.1 or later. These potentially breaking behavior changes are generally in how
the MVC subsystem behaves and how your code is called by the runtime. By opting in, you get the latest
behavior, and the long-term behavior of ASP.NET Core.
The following code sets the compatibility mode to ASP.NET Core 2.1:
We recommend you test your app using the latest version ( CompatibilityVersion.Version_2_1 ). We anticipate that
most apps won't have breaking behavior changes using the latest version.
Apps that call SetCompatibilityVersion(CompatibilityVersion.Version_2_0) are protected from potentially
breaking behavior changes introduced in the ASP.NET Core 2.1 MVC and later 2.x versions. This protection:
Does not apply to all 2.1 and later changes, it's targeted to potentially breaking ASP.NET Core runtime
behavior changes in the MVC subsystem.
Does not extend to the next major version.
The default compatibility for ASP.NET Core 2.1 and later 2.x apps that do not call SetCompatibilityVersion is 2.0
compatibility. That is, not calling SetCompatibilityVersion is the same as calling
SetCompatibilityVersion(CompatibilityVersion.Version_2_0) .
The following code sets the compatibility mode to ASP.NET Core 2.1, except for the following behaviors:
AllowCombiningAuthorizeFilters
InputFormatterExceptionPolicy
For apps that encounter breaking behavior changes, using the appropriate compatibility switches:
Allows you to use the latest release and opt out of specific breaking behavior changes.
Gives you time to update your app so it works with the latest changes.
The MvcOptions class source comments have a good explanation of what changed and why the changes are an
improvement for most users.
At some future date, there will be an ASP.NET Core 3.0 version. Old behaviors supported by compatibility
switches will be removed in the 3.0 version. We feel these are positive changes benefitting nearly all users. By
introducing these changes now, most apps can benefit now, and the others will have time to update their apps.
Build web APIs with ASP.NET Core
8/22/2018 • 4 minutes to read • Edit Online
By Scott Addie
View or download sample code (how to download)
This document explains how to build a web API in ASP.NET Core and when it's most appropriate to use each
feature.
[HttpGet]
public async Task<ActionResult<List<Pet>>> GetAllAsync()
{
return await _repository.GetPetsAsync();
}
[HttpGet("{id}")]
[ProducesResponseType(404)]
public async Task<ActionResult<Pet>> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);
if (pet == null)
{
return NotFound();
}
return pet;
}
[HttpPost]
[ProducesResponseType(400)]
public async Task<ActionResult<Pet>> CreateAsync(Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddPetAsync(pet);
return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}
[Produces("application/json")]
[Route("api/[controller]")]
public class PetsController : ControllerBase
{
private readonly PetsRepository _repository;
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Pet>), 200)]
public async Task<IActionResult> GetAllAsync()
{
var pets = await _repository.GetPetsAsync();
return Ok(pets);
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(Pet), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetByIdAsync(int id)
{
var pet = await _repository.GetPetAsync(id);
if (pet == null)
{
return NotFound();
}
return Ok(pet);
}
[HttpPost]
[ProducesResponseType(typeof(Pet), 201)]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Pet pet)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddPetAsync(pet);
return CreatedAtAction(nameof(GetByIdAsync),
new { id = pet.Id }, pet);
}
}
The ControllerBase class provides access to several properties and methods. In the preceding code, examples
include BadRequest(ModelStateDictionary) and CreatedAtAction(String, Object, Object). These methods are
called within action methods to return HTTP 400 and 201 status codes, respectively. The ModelState property,
also provided by ControllerBase , is accessed to handle request model validation.
A compatibility version of 2.1 or later, set via SetCompatibilityVersion, is required to use this attribute. For
example, the highlighted code in Startup.ConfigureServices sets the 2.1 compatibility flag:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
For more information, see Compatibility version for ASP.NET Core MVC.
The [ApiController] attribute is commonly coupled with ControllerBase to enable REST-specific behavior for
controllers. ControllerBase provides access to methods such as NotFound and File.
Another approach is to create a custom base controller class annotated with the [ApiController] attribute:
[ApiController]
public class MyBaseController
{
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
The default behavior is disabled when the SuppressModelStateInvalidFilter property is set to true . Add the
following code in Startup.ConfigureServices after
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
WARNING
Don't use [FromRoute] when values might contain %2f (that is / ). %2f won't be unescaped to / . Use
[FromQuery] if the value might contain %2f .
Without the [ApiController] attribute, binding source attributes are explicitly defined. In the following example,
the [FromQuery] attribute indicates that the discontinuedOnly parameter value is provided in the request URL's
query string:
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAsync(
[FromQuery] bool discontinuedOnly = false)
{
List<Product> products = null;
if (discontinuedOnly)
{
products = await _repository.GetDiscontinuedProductsAsync();
}
else
{
products = await _repository.GetProductsAsync();
}
return products;
}
Inference rules are applied for the default data sources of action parameters. These rules configure the binding
sources you're otherwise likely to manually apply to the action parameters. The binding source attributes behave
as follows:
[FromBody] is inferred for complex type parameters. An exception to this rule is any complex, built-in type
with a special meaning, such as IFormCollection and CancellationToken. The binding source inference code
ignores those special types. [FromBody] isn't inferred for simple types such as string or int . Therefore, the
[FromBody] attribute should be used for simple types when that functionality is desired. When an action has
more than one parameter explicitly specified (via [FromBody] ) or inferred as bound from the request body, an
exception is thrown. For example, the following action signatures cause an exception:
// Don't do this. All of the following actions result in an exception.
[HttpPost]
public IActionResult Action1(Product product,
Order order) => null;
[HttpPost]
public IActionResult Action2(Product product,
[FromBody] Order order) => null;
[HttpPost]
public IActionResult Action3([FromBody] Product product,
[FromBody] Order order) => null;
[FromForm ] is inferred for action parameters of type IFormFile and IFormFileCollection. It's not inferred for
any simple or user-defined types.
[FromRoute] is inferred for any action parameter name matching a parameter in the route template. When
more than one route matches an action parameter, any route value is considered [FromRoute] .
[FromQuery] is inferred for any other action parameters.
The default inference rules are disabled when the SuppressInferBindingSourcesForParameters property is set to
true . Add the following code in Startup.ConfigureServices after
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); :
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
By Scott Addie
View or download sample code (how to download)
ASP.NET Core offers the following options for Web API controller action return types:
Specific type
IActionResult
ActionResult<T>
Specific type
IActionResult
This document explains when it's most appropriate to use each return type.
Specific type
The simplest action returns a primitive or complex data type (for example, string or a custom object type).
Consider the following action, which returns a collection of custom Product objects:
[HttpGet]
public IEnumerable<Product> Get()
{
return _repository.GetProducts();
}
Without known conditions to safeguard against during action execution, returning a specific type could suffice.
The preceding action accepts no parameters, so parameter constraints validation isn't needed.
When known conditions need to be accounted for in an action, multiple return paths are introduced. In such a
case, it's common to mix an ActionResult return type with the primitive or complex return type. Either
IActionResult or ActionResult<T> are necessary to accommodate this type of action.
IActionResult type
The IActionResult return type is appropriate when multiple ActionResult return types are possible in an action.
The ActionResult types represent various HTTP status codes. Some common return types falling into this
category are BadRequestResult (400), NotFoundResult (404), and OkObjectResult (200).
Because there are multiple return types and paths in the action, liberal use of the [ProducesResponseType]
attribute is necessary. This attribute produces more descriptive response details for API help pages generated by
tools like Swagger. [ProducesResponseType] indicates the known types and HTTP status codes to be returned by
the action.
Synchronous action
Consider the following synchronous action in which there are two possible return types:
[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(Product))]
[ProducesResponseType(404)]
public IActionResult GetById(int id)
{
if (!_repository.TryGetProduct(id, out var product))
{
return NotFound();
}
return Ok(product);
}
In the preceding action, a 404 status code is returned when the product represented by id doesn't exist in the
underlying data store. The NotFound helper method is invoked as a shortcut to return new NotFoundResult(); . If
the product does exist, a Product object representing the payload is returned with a 200 status code. The Ok
helper method is invoked as the shorthand form of return new OkObjectResult(product); .
Asynchronous action
Consider the following asynchronous action in which there are two possible return types:
[HttpPost]
[ProducesResponseType(201, Type = typeof(Product))]
[ProducesResponseType(400)]
public async Task<IActionResult> CreateAsync([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddProductAsync(product);
In the preceding action, a 400 status code is returned when model validation fails and the BadRequest helper
method is invoked. For example, the following model indicates that requests must provide the Name property
and a value. Therefore, failure to provide a proper Name in the request causes model validation to fail.
[Required]
public string Name { get; set; }
The preceding action's other known return code is a 201, which is generated by the CreatedAtAction helper
method. In this path, the Product object is returned.
ActionResult<T> type
ASP.NET Core 2.1 introduces the ActionResult<T> return type for Web API controller actions. It enables you to
return a type deriving from ActionResult or return a specific type. ActionResult<T> offers the following benefits
over the IActionResult type:
The [ProducesResponseType] attribute's property can be excluded. For example,
Type
[ProducesResponseType(200, Type = typeof(Product))] is simplified to [ProducesResponseType(200)] . The
action's expected return type is instead inferred from the T in ActionResult<T> .
Implicit cast operators support the conversion of both T and ActionResult to ActionResult<T> . T converts
to ObjectResult, which means return new ObjectResult(T); is simplified to return T; .
C# doesn't support implicit cast operators on interfaces. Consequently, conversion of the interface to a concrete
type is necessary to use ActionResult<T> . For example, use of IEnumerable in the following example doesn't
work:
[HttpGet]
public ActionResult<IEnumerable<Product>> Get()
{
return _repository.GetProducts();
}
[HttpGet("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public ActionResult<Product> GetById(int id)
{
if (!_repository.TryGetProduct(id, out var product))
{
return NotFound();
}
return product;
}
In the preceding code, a 404 status code is returned when the product doesn't exist in the database. If the product
does exist, the corresponding Product object is returned. Before ASP.NET Core 2.1, the return product; line
would have been return Ok(product); .
TIP
As of ASP.NET Core 2.1, action parameter binding source inference is enabled when a controller class is decorated with the
[ApiController] attribute. A parameter name matching a name in the route template is automatically bound using the
request route data. Consequently, the preceding action's id parameter isn't explicitly annotated with the [FromRoute]
attribute.
Asynchronous action
Consider an asynchronous action in which there are two possible return types:
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public async Task<ActionResult<Product>> CreateAsync(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _repository.AddProductAsync(product);
If model validation fails, the BadRequest method is invoked to return a 400 status code. The ModelState property
containing the specific validation errors is passed to it. If model validation succeeds, the product is created in the
database. A 201 status code is returned.
TIP
As of ASP.NET Core 2.1, action parameter binding source inference is enabled when a controller class is decorated with the
[ApiController] attribute. Complex type parameters are automatically bound using the request body. Consequently,
the preceding action's product parameter isn't explicitly annotated with the [FromBody] attribute.
Additional resources
Handle requests with controllers in ASP.NET Core MVC
Model validation in ASP.NET Core MVC
ASP.NET Core Web API help pages with Swagger / OpenAPI
Advanced topics for ASP.NET Core Web API
7/10/2018 • 2 minutes to read • Edit Online
Custom formatters
Format response data
Custom formatters in ASP.NET Core Web API
6/21/2018 • 4 minutes to read • Edit Online
By Tom Dykstra
ASP.NET Core MVC has built-in support for data exchange in web APIs by using JSON, XML, or plain text
formats. This article shows how to add support for additional formats by creating custom formatters.
View or download sample code (how to download)
The following sections provide guidance and code examples for each of these steps.
For binary types, derive from the InputFormatter or OutputFormatter base class.
Specify valid media types and encodings
In the constructor, specify valid media types and encodings by adding to the SupportedMediaTypes and
SupportedEncodings collections.
public VcardOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
NOTE
You can't do constructor dependency injection in a formatter class. For example, you can't get a logger by adding a logger
parameter to the constructor. To access services, you have to use the context object that gets passed in to your methods. A
code example below shows how to do this.
Override CanReadType/CanWriteType
Specify the type you can deserialize into or serialize from by overriding the CanReadType or CanWriteType
methods. For example, you might only be able to create vCard text from a Contact type and vice versa.
services.AddMvc(options =>
{
options.InputFormatters.Insert(0, new VcardInputFormatter());
options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});
Formatters are evaluated in the order you insert them. The first one takes precedence.
Next steps
See the sample application, which implements simple vCard input and output formatters. The application reads
and writes vCards that look like the following example:
BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
UID:20293482-9240-4d68-b475-325df4a83728
END:VCARD
To see vCard output, run the application and send a Get request with Accept header "text/vcard" to
(when running from Visual Studio) or
http://localhost:63313/api/contacts/ http://localhost:5000/api/contacts/
(when running from the command line).
To add a vCard to the in-memory collection of contacts, send a Post request to the same URL, with Content-Type
header "text/vcard" and with vCard text in the body, formatted like the example above.
Format response data in ASP.NET Core Web API
6/21/2018 • 8 minutes to read • Edit Online
By Steve Smith
ASP.NET Core MVC has built-in support for formatting response data, using fixed formats or in response to
client specifications.
View or download sample code (how to download)
NOTE
An action isn't required to return any particular type; MVC supports any object return value. If an action returns an
IActionResult implementation and the controller inherits from Controller , developers have many helper methods
corresponding to many of the choices. Results from actions that return objects that are not IActionResult types will be
serialized using the appropriate IOutputFormatter implementation.
To return data in a specific format from a controller that inherits from the Controller base class, use the built-in
helper method Json to return JSON and Content for plain text. Your action method should return either the
specific result type (for instance, JsonResult ) or IActionResult .
Returning JSON -formatted data:
// GET: api/authors
[HttpGet]
public JsonResult Get()
{
return Json(_authorRepository.List());
}
// GET api/authors/about
[HttpGet("About")]
public ContentResult About()
{
return Content("An API listing authors of docs.asp.net.");
}
// GET api/authors/version
[HttpGet("version")]
public string Version()
{
return "Version 1.0.0";
}
TIP
For non-trivial actions with multiple return types or options (for example, different HTTP status codes based on the result of
operations performed), prefer IActionResult as the return type.
Content Negotiation
Content negotiation (conneg for short) occurs when the client specifies an Accept header. The default format used
by ASP.NET Core MVC is JSON. Content negotiation is implemented by ObjectResult . It's also built into the
status code specific action results returned from the helper methods (which are all based on ObjectResult ). You
can also return a model type (a class you've defined as your data transfer type) and the framework will
automatically wrap it in an ObjectResult for you.
The following action method uses the Ok and NotFound helper methods:
// GET: api/authors/search?namelike=th
[HttpGet("Search")]
public IActionResult Search(string namelike)
{
var result = _authorRepository.GetByNameSubstring(namelike);
if (!result.Any())
{
return NotFound(namelike);
}
return Ok(result);
}
A JSON -formatted response will be returned unless another format was requested and the server can return the
requested format. You can use a tool like Fiddler to create a request that includes an Accept header and specify
another format. In that case, if the server has a formatter that can produce a response in the requested format,
the result will be returned in the client-preferred format.
In the above screenshot, the Fiddler Composer has been used to generate a request, specifying
Accept: application/xml . By default, ASP.NET Core MVC only supports JSON, so even when another format is
specified, the result returned is still JSON -formatted. You'll see how to add additional formatters in the next
section.
Controller actions can return POCOs (Plain Old CLR Objects), in which case ASP.NET Core MVC automatically
creates an ObjectResult for you that wraps the object. The client will get the formatted serialized object (JSON
format is the default; you can configure XML or other formats). If the object being returned is null , then the
framework will return a 204 No Content response.
Returning an object type:
// GET api/authors/ardalis
[HttpGet("{alias}")]
public Author Get(string alias)
{
return _authorRepository.GetByAlias(alias);
}
In the sample, a request for a valid author alias will receive a 200 OK response with the author's data. A request
for an invalid alias will receive a 204 No Content response. Screenshots showing the response in XML and JSON
formats are shown below.
Content Negotiation Process
Content negotiation only takes place if an Accept header appears in the request. When a request contains an
accept header, the framework will enumerate the media types in the accept header in preference order and will try
to find a formatter that can produce a response in one of the formats specified by the accept header. In case no
formatter is found that can satisfy the client's request, the framework will try to find the first formatter that can
produce a response (unless the developer has configured the option on MvcOptions to return 406 Not Acceptable
instead). If the request specifies XML, but the XML formatter has not been configured, then the JSON formatter
will be used. More generally, if no formatter is configured that can provide the requested format, then the first
formatter that can format the object is used. If no header is given, the first formatter that can handle the object to
be returned will be used to serialize the response. In this case, there isn't any negotiation taking place - the server
is determining what format it will use.
NOTE
If the Accept header contains */* , the Header will be ignored unless RespectBrowserAcceptHeader is set to true on
MvcOptions .
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true; // false by default
});
Configuring Formatters
If your application needs to support additional formats beyond the default of JSON, you can add NuGet
packages and configure MVC to support them. There are separate formatters for input and output. Input
formatters are used by Model Binding; output formatters are used to format responses. You can also configure
Custom Formatters.
Adding XML Format Support
To add support for XML formatting, install the Microsoft.AspNetCore.Mvc.Formatters.Xml NuGet package.
Add the XmlSerializerFormatters to MVC's configuration in Startup.cs:
services.AddMvc()
.AddXmlSerializerFormatters();
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
These two approaches will serialize results using System.Xml.Serialization.XmlSerializer . If you prefer, you can
use the System.Runtime.Serialization.DataContractSerializer by adding its associated formatter:
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
});
Once you've added support for XML formatting, your controller methods should return the appropriate format
based on the request's Accept header, as this Fiddler example demonstrates:
You can see in the Inspectors tab that the Raw GET request was made with an Accept: application/xml header
set. The response pane shows the Content-Type: application/xml header, and the Author object has been
serialized to XML.
Use the Composer tab to modify the request to specify application/json in the Accept header. Execute the
request, and the response will be formatted as JSON:
In this screenshot, you can see the request sets a header of Accept: application/json and the response specifies
the same as its Content-Type . The Author object is shown in the body of the response, in JSON format.
Forcing a Particular Format
If you would like to restrict the response formats for a specific action you can, you can apply the [Produces] filter.
The [Produces] filter specifies the response formats for a specific action (or controller). Like most Filters, this can
be applied at the action, controller, or global scope.
[Produces("application/json")]
public class AuthorsController
The [Produces] filter will force all actions within the AuthorsController to return JSON -formatted responses,
even if other formatters were configured for the application and the client provided an Accept header requesting
a different, available format. See Filters to learn more, including how to apply filters globally.
Special Case Formatters
Some special cases are implemented using built-in formatters. By default, string return types will be formatted
as text/plain (text/html if requested via Accept header). This behavior can be removed by removing the
TextOutputFormatter . You remove formatters in the Configure method in Startup.cs (shown below ). Actions that
have a model object return type will return a 204 No Content response when returning null . This behavior can
be removed by removing the HttpNoContentOutputFormatter . The following code removes the
TextOutputFormatter and HttpNoContentOutputFormatter .
services.AddMvc(options =>
{
options.OutputFormatters.RemoveType<TextOutputFormatter>();
options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});
Without the TextOutputFormatter , string return types return 406 Not Acceptable, for example. Note that if an
XML formatter exists, it will format string return types if the TextOutputFormatter is removed.
Without the HttpNoContentOutputFormatter , null objects are formatted using the configured formatter. For
example, the JSON formatter will simply return a response with a body of null , while the XML formatter will
return an empty XML element with the attribute xsi:nil="true" set.
[FormatFilter]
public class ProductsController
{
[Route("[controller]/[action]/{id}.{format?}")]
public Product GetById(int id)
This route would allow the requested format to be specified as an optional file extension. The [FormatFilter]
attribute checks for the existence of the format value in the RouteData and will map the response format to the
appropriate formatter when the response is created.
ROUTE FORMATTER
Introduction
Get started
Server concepts
Supported platforms
Hubs
HubContext
Users and groups
Publish to Azure
Clients
.NET client
Java client
Java API reference
JavaScript client
JavaScript API reference
WebPack and TypeScript
Configuration
Authentication and authorization
Security considerations
MessagePack Hub Protocol
Streaming
Differences between SignalR versions
Introduction to ASP.NET Core SignalR
7/30/2018 • 2 minutes to read • Edit Online
What is SignalR?
ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-
time web functionality enables server-side code to push content to clients instantly.
Good candidates for SignalR:
Apps that require high frequency updates from the server. Examples are gaming, social networks, voting,
auction, maps, and GPS apps.
Dashboards and monitoring apps. Examples include company dashboards, instant sales updates, or travel
alerts.
Collaborative apps. Whiteboard apps and team meeting software are examples of collaborative apps.
Apps that require notifications. Social networks, email, chat, games, travel alerts, and many other apps use
notifications.
SignalR provides an API for creating server-to-client remote procedure calls (RPC ). The RPCs call JavaScript
functions on clients from server-side .NET Core code.
Here are some features of SignalR for ASP.NET Core:
Handles connection management automatically.
Sends messages to all connected clients simultaneously. For example, a chat room.
Sends messages to specific clients or groups of clients.
Scales to handle increasing traffic.
The source is hosted in a SignalR repository on GitHub.
Transports
SignalR supports several techniques for handling real-time communications:
WebSockets
Server-Sent Events
Long Polling
SignalR automatically chooses the best transport method that is within the capabilities of the server and client.
Hubs
SignalR uses hubs to communicate between clients and servers.
A hub is a high-level pipeline that allows a client and server to call methods on each other. SignalR handles the
dispatching across machine boundaries automatically, allowing clients to call methods on the server and vice
versa. You can pass strongly-typed parameters to methods, which enables model binding. SignalR provides two
built-in hub protocols: a text protocol based on JSON and a binary protocol based on MessagePack.
MessagePack generally creates smaller messages compared to JSON. Older browsers must support XHR level 2
to provide MessagePack protocol support.
Hubs call client-side code by sending messages that contain the name and parameters of the client-side method.
Objects sent as method parameters are deserialized using the configured protocol. The client tries to match the
name to a method in the client-side code. When the client finds a match, it calls the method and passes to it the
deserialized parameter data.
Additional resources
Get started with SignalR for ASP.NET Core
Supported Platforms
Hubs
JavaScript client
Tutorial: Get started with SignalR on ASP.NET Core
9/27/2018 • 6 minutes to read • Edit Online
This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:
Create a web app that uses SignalR on ASP.NET Core.
Create a SignalR hub on the server.
Connect to the SignalR hub from JavaScript clients.
Use the hub to send messages from any client to all connected clients.
At the end, you'll have a working chat app:
Prerequisites
Visual Studio
Visual Studio Code
Visual Studio for Mac
Visual Studio 2017 version 15.8 or later with the ASP.NET and web development workload
.NET Core SDK 2.1 or later
Select Choose specific files, expand the dist/browser folder, and select signalr.js and signalr.min.js.
Set Target Location to wwwroot/lib/signalr/, and select Install.
LibMan creates a wwwroot/lib/signalr folder and copies the selected files to it.
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
The ChatHub class inherits from the SignalR Hub class. The Hub class manages connections, groups, and
messaging.
The SendMessage method can be called by any connected client. It sends the received message to all
clients. SignalR code is asynchronous to provide maximum scalability.
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for
a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request
pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
});
app.UseMvc();
}
}
}
These changes add SignalR to the dependency injection system and the middleware pipeline.
@page
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
User..........<input type="text" id="userInput" />
<br />
Message...<input type="text" id="messageInput" />
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6"> </div>
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
"use strict";
connection.start().catch(function (err) {
return console.error(err.toString());
});
TIP
If the app doesn't work, open your browser developer tools (F12) and go to the console. You might see errors related to
your HTML and JavaScript code. For example, suppose you put signalr.js in a different folder than directed. In that case the
reference to that file won't work and you'll see a 404 error in the console.
Next steps
If you want clients to connect to a SignalR app from different domains, you have to enable Cross-Origin
Resource Sharing (CORS ). For more information, see Cross-origin resource sharing.
To learn more about SignalR, hubs, and JavaScript clients, see these resources:
Introduction to SignalR for ASP.NET Core
Use hubs in SignalR for ASP.NET Core
ASP.NET Core SignalR JavaScript client
Use hubs in SignalR for ASP.NET Core
9/13/2018 • 5 minutes to read • Edit Online
services.AddSignalR();
When adding SignalR functionality to an ASP.NET Core app, setup SignalR routes by calling app.UseSignalR in the
Startup.Configure method.
app.UseSignalR(route =>
{
route.MapHub<ChatHub>("/chathub");
});
You can specify a return type and parameters, including complex types and arrays, as you would in any C# method.
SignalR handles the serialization and deserialization of complex objects and arrays in your parameters and return
values.
PROPERTY DESCRIPTION
METHOD DESCRIPTION
PROPERTY DESCRIPTION
Caller Calls a method on the client that invoked the hub method
Others Calls a method on all connected clients except the client that
invoked the method
METHOD DESCRIPTION
AllExcept Calls a method on all connected clients except for the specified
connections
Each property or method in the preceding tables returns an object with a SendAsync method. The SendAsync
method allows you to supply the name and parameters of the client method to call.
Using Hub<IChatClient> enables compile-time checking of the client methods. This prevents issues caused by using
magic strings, since Hub<T> can only provide access to the methods defined in the interface.
Using a strongly typed Hub<T> disables the ability to use SendAsync .
Handle errors
Exceptions thrown in your hub methods are sent to the client that invoked the method. On the JavaScript client, the
invoke method returns a JavaScript Promise. When the client receives an error with a handler attached to the
promise using catch , it's invoked and passed as a JavaScript Error object.
Related resources
Intro to ASP.NET Core SignalR
JavaScript client
Publish to Azure
ASP.NET Core SignalR supported platforms
7/16/2018 • 2 minutes to read • Edit Online
BROWSER VERSION
services.AddSignalR();
When adding SignalR functionality to an ASP.NET Core app, setup SignalR routes by calling app.UseSignalR
in the Startup.Configure method.
app.UseSignalR(route =>
{
route.MapHub<ChatHub>("/chathub");
});
You can specify a return type and parameters, including complex types and arrays, as you would in any C#
method. SignalR handles the serialization and deserialization of complex objects and arrays in your parameters
and return values.
PROPERTY DESCRIPTION
METHOD DESCRIPTION
PROPERTY DESCRIPTION
Caller Calls a method on the client that invoked the hub method
Others Calls a method on all connected clients except the client that
invoked the method
METHOD DESCRIPTION
Each property or method in the preceding tables returns an object with a SendAsync method. The SendAsync
method allows you to supply the name and parameters of the client method to call.
Using Hub<IChatClient> enables compile-time checking of the client methods. This prevents issues caused by
using magic strings, since Hub<T> can only provide access to the methods defined in the interface.
Using a strongly typed Hub<T> disables the ability to use SendAsync .
Handle errors
Exceptions thrown in your hub methods are sent to the client that invoked the method. On the JavaScript
client, the invoke method returns a JavaScript Promise. When the client receives an error with a handler
attached to the promise using catch , it's invoked and passed as a JavaScript Error object.
Related resources
Intro to ASP.NET Core SignalR
JavaScript client
Publish to Azure
Send messages from outside a hub
9/10/2018 • 2 minutes to read • Edit Online
By Mikael Mengistu
The SignalR hub is the core abstraction for sending messages to clients connected to the SignalR server. It's also
possible to send messages from other places in your app using the IHubContext service. This article explains how
to access a SignalR IHubContext to send notifications to clients from outside a hub.
View or download sample code (how to download)
NOTE
This differs from ASP.NET 4.x SignalR which used GlobalHost to provide access to the IHubContext . ASP.NET Core has a
dependency injection framework that removes the need for this global singleton.
Now, with access to an instance of IHubContext , you can call hub methods as if you were in the hub itself.
NOTE
When hub methods are called from outside of the Hub class, there's no caller associated with the invocation. Therefore,
there's no access to the ConnectionId , Caller , and Others properties.
Related resources
Get started
Hubs
Publish to Azure
Manage users and groups in SignalR
7/16/2018 • 2 minutes to read • Edit Online
By Brennan Conroy
SignalR allows messages to be sent to all connections associated with a specific user, as well as to named groups
of connections.
View or download sample code (how to download)
Users in SignalR
SignalR allows you to send messages to all connections associated with a specific user. By default, SignalR uses
the ClaimTypes.NameIdentifier from the ClaimsPrincipal associated with the connection as the user identifier. A
single user can have multiple connections to a SignalR app. For example, a user could be connected on their
desktop as well as their phone. Each device has a separate SignalR connection, but they're all associated with the
same user. If a message is sent to the user, all of the connections associated with that user receive the message.
The user identifier for a connection can be accessed by the Context.UserIdentifier property in your hub.
Send a message to a specific user by passing the user identifier to the User function in your hub method as
shown in the following example:
NOTE
The user identifier is case-sensitive.
The user identifier can be customized by creating an IUserIdProvider , and registering it in ConfigureServices .
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
}
NOTE
AddSignalR must be called before registering your custom SignalR services.
Groups in SignalR
A group is a collection of connections associated with a name. Messages can be sent to all connections in a group.
Groups are the recommended way to send to a connection or multiple connections because the groups are
managed by the application. A connection can be a member of multiple groups. This makes groups ideal for
something like a chat application, where each room can be represented as a group. Connections can be added to
or removed from groups via the AddToGroupAsync and RemoveFromGroupAsync methods.
Group membership isn't preserved when a connection reconnects. The connection needs to rejoin the group when
it's re-established. It's not possible to count the members of a group, since this information is not available if the
application is scaled to multiple servers.
NOTE
Group names are case-sensitive.
Related resources
Get started
Hubs
Publish to Azure
Publish an ASP.NET Core SignalR app to an Azure
Web App
7/16/2018 • 2 minutes to read • Edit Online
Azure Web App is a Microsoft cloud computing platform service for hosting web apps, including ASP.NET Core.
NOTE
This article refers to publishing an ASP.NET Core SignalR app from Visual Studio. Visit SignalR service for Azure for more
information about using SignalR on Azure.
Enter the following information in the Create App Service dialog and select Create.
ITEM DESCRIPTION
Resource Group The group of related resources to which the app belongs.
If an HTTP 502.2 error occurs, see Deploy ASP.NET Core preview release to Azure App Service to resolve it.
Related resources
Publish an ASP.NET Core app to Azure with command line tools
Publish an ASP.NET Core app to Azure with Visual Studio
Host and deploy ASP.NET Core Preview apps on Azure
ASP.NET Core SignalR JavaScript client
9/20/2018 • 3 minutes to read • Edit Online
By Rachel Appel
The ASP.NET Core SignalR JavaScript client library enables developers to call server-side hub code.
View or download sample code (how to download)
npm init -y
npm install @aspnet/signalr
npm installs the package contents in the node_modules\@aspnet\signalr\dist\browser folder. Create a new folder
named signalr under the wwwroot\lib folder. Copy the signalr.js file to the wwwroot\lib\signalr folder.
<script src="~/lib/signalr/signalr.js"></script>
Connect to a hub
The following code creates and starts a connection. The hub's name is case insensitive.
Cross-origin connections
Typically, browsers load connections from the same domain as the requested page. However, there are occasions
when a connection to another domain is required.
To prevent a malicious site from reading sensitive data from another site, cross-origin connections are disabled by
default. To allow a cross-origin request, enable it in the Startup class.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SignalRChat.Hubs;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
services.AddMvc();
services.AddSignalR();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseCors("CorsPolicy");
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc();
}
}
}
Call hub methods from client
JavaScript clients call public methods on hubs via the invoke method of the HubConnection. The invoke method
accepts two arguments:
The name of the hub method. In the following example, the method name on the hub is SendMessage .
Any arguments defined in the hub method. In the following example, the argument name is message .
The preceding code in connection.on runs when server-side code calls it using the SendAsync method.
SignalR determines which client method to call by matching the method name and arguments defined in
SendAsync and connection.on .
NOTE
As a best practice, call the start method on the HubConnection after on . Doing so ensures your handlers are registered
before any messages are received.
Setup client-side log tracing by passing a logger and type of event to log when the connection is made. Messages
are logged with the specified log level and higher. Available log levels are as follows:
signalR.LogLevel.Error – Error messages. Logs Error messages only.
signalR.LogLevel.Warning – Warning messages about potential errors. Logs Warning , and Error messages.
signalR.LogLevel.Information – Status messages without errors. Logs Information , Warning , and Error
messages.
signalR.LogLevel.Trace – Trace messages. Logs everything, including data transported between hub and client.
Use the configureLogging method on HubConnectionBuilder to configure the log level. Messages are logged to the
browser console.
Additional resources
JavaScript API reference
Hubs
.NET client
Publish to Azure
Enable Cross-Origin Requests (CORS ) in ASP.NET Core
ASP.NET Core SignalR .NET Client
9/12/2018 • 2 minutes to read • Edit Online
The ASP.NET Core SignalR .NET client library lets you communicate with SignalR hubs from .NET apps.
NOTE
Xamarin has special requirements for Visual Studio version. For more information, see SignalR Client 2.1.1 in Xamarin.
Install-Package Microsoft.AspNetCore.SignalR.Client
Connect to a hub
To establish a connection, create a HubConnectionBuilder and call Build . The hub URL, protocol, transport type,
log level, headers, and other options can be configured while building a connection. Configure any required
options by inserting any of the HubConnectionBuilder methods into Build . Start the connection with StartAsync .
using System;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.AspNetCore.SignalR.Client;
namespace SignalRChatClient
{
public partial class MainWindow : Window
{
HubConnection connection;
public MainWindow()
{
InitializeComponent();
try
{
await connection.StartAsync();
messagesList.Items.Add("Connection started");
connectButton.IsEnabled = false;
sendButton.IsEnabled = true;
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
The main reason for the async support is so you can restart the connection. Starting a connection is an async
action.
In a Closed handler that restarts the connection, consider waiting for some random delay to prevent overloading
the server, as shown in the following example:
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
The preceding code in connection.On runs when server-side code calls it using the SendAsync method.
try
{
await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch (Exception ex)
{
messagesList.Items.Add(ex.Message);
}
Additional resources
Hubs
JavaScript client
Publish to Azure
ASP.NET Core SignalR Java Client
9/20/2018 • 2 minutes to read • Edit Online
By Mikael Mengistu
The Java client enables connecting to an ASP.NET Core SignalR server from Java code, including Android apps.
Like the JavaScript client and the .NET client, the Java client enables you to receive and send messages to a hub in
real time. The Java client is available in ASP.NET Core 2.2 and later.
The sample Java console app referenced in this article uses the SignalR Java client.
View or download sample code (how to download)
implementation 'com.microsoft.aspnet:signalr:0.1.0-preview2-35174'
If using Maven, add the following lines inside the <dependencies> element of your pom.xml file:
<dependency>
<groupId>com.microsoft.aspnet</groupId>
<artifactId>signalr</artifactId>
<version>0.1.0-preview2-35174</version>
</dependency>
Connect to a hub
To establish a HubConnection , the HubConnectionBuilder should be used. The hub URL and log level can be
configured while building a connection. Configure any required options by calling any of the HubConnectionBuilder
methods before build . Start the connection with start .
hubConnection.send("Send", input);
Known limitations
This is an early preview release of the Java client. There are many features that aren't supported yet. The following
gaps are being worked on for future releases:
Only primitive types can be accepted as parameters and return types.
The APIs are synchronous.
Only the "Send" call type is supported at this time. "Invoke" and streaming of return values aren't supported.
Only the JSON protocol is supported.
Only the WebSockets transport is supported.
Additional resources
Java API reference
Use hubs in ASP.NET Core SignalR
ASP.NET Core SignalR JavaScript client
Publish an ASP.NET Core SignalR app to Azure Web App
ASP.NET Core SignalR JavaScript client
9/20/2018 • 3 minutes to read • Edit Online
By Rachel Appel
The ASP.NET Core SignalR JavaScript client library enables developers to call server-side hub code.
View or download sample code (how to download)
npm init -y
npm install @aspnet/signalr
npm installs the package contents in the node_modules\@aspnet\signalr\dist\browser folder. Create a new
folder named signalr under the wwwroot\lib folder. Copy the signalr.js file to the wwwroot\lib\signalr folder.
<script src="~/lib/signalr/signalr.js"></script>
Connect to a hub
The following code creates and starts a connection. The hub's name is case insensitive.
Cross-origin connections
Typically, browsers load connections from the same domain as the requested page. However, there are
occasions when a connection to another domain is required.
To prevent a malicious site from reading sensitive data from another site, cross-origin connections are disabled
by default. To allow a cross-origin request, enable it in the Startup class.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SignalRChat.Hubs;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
services.AddMvc();
services.AddSignalR();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseCors("CorsPolicy");
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc();
}
}
}
Call hub methods from client
JavaScript clients call public methods on hubs via the invoke method of the HubConnection. The invoke
method accepts two arguments:
The name of the hub method. In the following example, the method name on the hub is SendMessage .
Any arguments defined in the hub method. In the following example, the argument name is message .
The preceding code in connection.on runs when server-side code calls it using the SendAsync method.
SignalR determines which client method to call by matching the method name and arguments defined in
SendAsync and connection.on .
NOTE
As a best practice, call the start method on the HubConnection after on . Doing so ensures your handlers are
registered before any messages are received.
Setup client-side log tracing by passing a logger and type of event to log when the connection is made.
Messages are logged with the specified log level and higher. Available log levels are as follows:
signalR.LogLevel.Error– Error messages. Logs Error messages only.
signalR.LogLevel.Warning – Warning messages about potential errors. Logs Warning , and Error
messages.
signalR.LogLevel.Information – Status messages without errors. Logs Information , Warning , and Error
messages.
signalR.LogLevel.Trace – Trace messages. Logs everything, including data transported between hub and
client.
Use the configureLogging method on HubConnectionBuilder to configure the log level. Messages are logged
to the browser console.
Additional resources
JavaScript API reference
Hubs
.NET client
Publish to Azure
Enable Cross-Origin Requests (CORS ) in ASP.NET Core
Use ASP.NET Core SignalR with TypeScript and
Webpack
7/19/2018 • 10 minutes to read • Edit Online
Prerequisites
Install the following software:
Visual Studio
.NET Core CLI
.NET Core SDK 2.1 or later
Node.js with npm
Visual Studio 2017 version 15.7.3 or later with the ASP.NET and web development workload
npm init -y
{
"name": "SignalRWebPack",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Setting the private property to true prevents package installation warnings in the next step.
3. Install the required npm packages. Execute the following command from the project root:
npm install -D -E [email protected] [email protected] [email protected] mini-css-
[email protected] [email protected] [email protected] [email protected] [email protected]
"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "wwwroot"),
filename: "[name].[chunkhash].js",
publicPath: "/"
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader"
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
plugins: [
new CleanWebpackPlugin(["wwwroot/*"]),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css"
})
]
};
The preceding file configures the Webpack compilation. Some configuration details to note:
The output property overrides the default value of dist. The bundle is instead emitted in the wwwroot
directory.
The resolve.extensions array includes .js to import the SignalR client JavaScript.
6. Create a new src directory in the project root. Its purpose is to store the project's client-side assets.
7. Create src/index.html with the following content.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR</title>
</head>
<body>
<div id="divMessages" class="messages">
</div>
<div class="input-zone">
<label id="lblMessage" for="tbMessage">Message:</label>
<input id="tbMessage" class="input-zone-input" type="text" />
<button id="btnSend">Send</button>
</div>
</body>
</html>
The preceding HTML defines the homepage's boilerplate markup.
8. Create a new src/css directory. Its purpose is to store the project's .css files.
9. Create src/css/main.css with the following content:
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
}
.input-zone {
align-items: center;
display: flex;
flex-direction: row;
margin: 10px;
}
.input-zone-input {
flex: 1;
margin-right: 10px;
}
.message-author {
font-weight: bold;
}
.messages {
border: 1px solid #000;
margin: 10px;
max-height: 300px;
min-height: 300px;
overflow-y: auto;
padding: 5px;
}
{
"compilerOptions": {
"target": "es5"
}
}
The preceding code configures the TypeScript compiler to produce ECMAScript 5-compatible JavaScript.
11. Create src/index.ts with the following content:
import "./css/main.css";
btnSend.addEventListener("click", send);
function send() {
}
The preceding TypeScript retrieves references to DOM elements and attaches two event handlers:
keyup : This event fires when the user types something in the textbox identified as tbMessage . The send
function is called when the user presses the Enter key.
click : This event fires when the user clicks the Send button. The send function is called.
app.UseDefaultFiles();
app.UseStaticFiles();
The preceding code allows the server to locate and serve the index.html file, whether the user enters its full
URL or the root URL of the web app.
2. Call AddSignalR in the Startup.ConfigureServices method. It adds the SignalR services to your project.
services.AddSignalR();
3. Map a /hub route to the ChatHub hub. Add the following lines at the end of the Startup.Configure method:
app.UseSignalR(options =>
{
options.MapHub<ChatHub>("/hub");
});
4. Create a new directory, called Hubs, in the project root. Its purpose is to store the SignalR hub, which is
created in the next step.
5. Create hub Hubs/ChatHub.cs with the following code:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
}
}
6. Add the following code at the top of the Startup.cs file to resolve the ChatHub reference:
using SignalRWebPack.Hubs;
The preceding command installs the SignalR TypeScript client, which allows the client to send messages to
the server.
2. Add the highlighted code to the src/index.ts file:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";
m.innerHTML =
`<div class="message__author">${username}</div><div>${message}</div>`;
divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});
btnSend.addEventListener("click", send);
function send() {
}
The preceding code supports receiving messages from the server. The HubConnectionBuilder class creates a
new builder for configuring the server connection. The withUrl function configures the hub URL.
SignalR enables the exchange of messages between a client and a server. Each message has a specific
name. For example, you can have messages with the name messageReceived that execute the logic
responsible for displaying the new message in the messages zone. Listening to a specific message can be
done via the on function. You can listen to any number of message names. It's also possible to pass
parameters to the message, such as the author's name and the content of the message received. Once the
client receives a message, a new div element is created with the author's name and the message content
in its innerHTML attribute. It's added to the main div element displaying the messages.
3. Now that the client can receive a message, configure it to send messages. Add the highlighted code to the
src/index.ts file:
import "./css/main.css";
import * as signalR from "@aspnet/signalr";
messageContainer.innerHTML =
`<div class="message-author">${username}</div><div>${message}</div>`;
divMessages.appendChild(messageContainer);
divMessages.scrollTop = divMessages.scrollHeight;
});
btnSend.addEventListener("click", send);
function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => tbMessage.value = "");
}
Sending a message through the WebSockets connection requires calling the send method. The method's
first parameter is the message name. The message data inhabits the other parameters. In this example, a
message identified as newMessage is sent to the server. The message consists of the username and the user
input from a text box. If the send works, the text box value is cleared.
4. Add the highlighted method to the ChatHub class:
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRWebPack.Hubs
{
public class ChatHub : Hub
{
public async Task NewMessage(string username, string message)
{
await Clients.All.SendAsync("messageReceived", username, message);
}
}
}
The preceding code broadcasts received messages to all connected users once the server receives them. It's
unnecessary to have a generic on method to receive all the messages. A method named after the message
name suffices.
In this example, the TypeScript client sends a message identified as newMessage . The C# NewMessage
method expects the data sent by the client. A call is made to the SendAsync method on Clients.All. The
received messages are sent to all clients connected to the hub.
This command yields the client-side assets to be served when running the app. The assets are placed in the
wwwroot folder.
Webpack completed the following tasks:
Purged the contents of the wwwroot directory.
Converted the TypeScript to JavaScript—a process known as transpilation.
Mangled the generated JavaScript to reduce file size—a process known as minification.
Copied the processed JavaScript, CSS, and HTML files from src to the wwwroot directory.
Injected the following elements into the wwwroot/index.html file:
A <link> tag, referencing the wwwroot/main.<hash>.css file. This tag is placed immediately
before the closing </head> tag.
A <script> tag, referencing the minified wwwroot/main.<hash>.js file. This tag is placed
immediately before the closing </body> tag.
2. Select Debug > Start without debugging to launch the app in a browser without attaching the debugger.
The wwwroot/index.html file is served at http://localhost:<port_number> .
3. Open another browser instance (any browser). Paste the URL in the address bar.
4. Choose either browser, type something in the Message text box, and click the Send button. The unique
user name and message are displayed on both pages instantly.
Additional resources
ASP.NET Core SignalR JavaScript client
Use hubs in ASP.NET Core SignalR
ASP.NET Core SignalR configuration
9/18/2018 • 9 minutes to read • Edit Online
services.AddSignalR()
.AddJsonProtocol(options => {
options.PayloadSerializerSettings.ContractResolver =
new DefaultContractResolver();
});
In the .NET client, the same AddJsonProtocol extension method exists on HubConnectionBuilder. The
Microsoft.Extensions.DependencyInjection namespace must be imported to resolve the extension method:
NOTE
It's not possible to configure JSON serialization in the JavaScript client at this time.
NOTE
It's not possible to configure MessagePack serialization in the JavaScript client at this time.
Options can be configured for all hubs by providing an options delegate to the AddSignalR call in
Startup.ConfigureServices .
Options for a single hub override the global options provided in AddSignalR and can be configured using
AddHubOptions<T>:
services.AddSignalR().AddHubOptions<MyHub>(options =>
{
options.EnableDetailedErrors = true;
});
Use HttpConnectionDispatcherOptions to configure advanced settings related to transports and memory buffer
management. These options are configured by passing a delegate to MapHub<T>.
AuthorizationData Data automatically gathered from the A list of IAuthorizeData objects used to
Authorize attributes applied to the determine if a client is authorized to
Hub class. connect to the hub.
The Long Polling transport has additional options that can be configured using the LongPolling property:
The WebSocket transport has additional options that can be configured using the WebSockets property:
NOTE
In order to register Logging providers, you must install the necessary packages. See the Built-in logging providers section of
the docs for a full list.
For example, to enable Console logging, install the Microsoft.Extensions.Logging.Console NuGet package. Call the
AddConsole extension method:
In the JavaScript client, a similar configureLogging method exists. Provide a LogLevel value indicating the
minimum level of log messages to produce. Logs are written to the browser console window.
NOTE
To disable logging entirely, specify signalR.LogLevel.None in the configureLogging method.
Log levels available to the JavaScript client are listed below. Setting the log level to one of these values enables
logging of messages at or above that level.
LEVEL DESCRIPTION
In the JavaScript client, transports are configured by setting the transport field on the options object provided to
withUrl :
In the JavaScript client, the access token is configured by setting the accessTokenFactory field on the options object
in withUrl :
In the .NET Client, timeout values are specified as TimeSpan values. In the JavaScript client, timeout values are
specified as a number indicating the duration in milliseconds.
Configure additional options
Additional options can be configured in the WithUrl ( withUrl in JavaScript) method on HubConnectionBuilder :
Options marked with an asterisk (*) aren't configurable in the JavaScript client, due to limitations in browser APIs.
In the .NET Client, these options can be modified by the options delegate provided to WithUrl :
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/myhub", options => {
options.Headers["Foo"] = "Bar";
options.Cookies.Add(new Cookie(/* ... */);
options.ClientCertificates.Add(/* ... */);
})
.Build();
In the JavaScript Client, these options can be provided in a JavaScript object provided to withUrl :
Additional resources
Tutorial: Get started with SignalR on ASP.NET Core
Use hubs in ASP.NET Core SignalR
ASP.NET Core SignalR JavaScript client
ASP.NET Core SignalR .NET Client
Use MessagePack Hub Protocol in SignalR for ASP.NET Core
ASP.NET Core SignalR supported platforms
Authentication and authorization in ASP.NET Core
SignalR
9/24/2018 • 5 minutes to read • Edit Online
By Andrew Stanton-Nurse
View or download sample code (how to download)
In the .NET client, there is a similar AccessTokenProvider property that can be used to configure the token:
In standard web APIs, bearer tokens are sent in an HTTP header. However, SignalR is unable to set these headers
in browsers when using some transports. When using WebSockets and Server-Sent Events, the token is
transmitted as a query string parameter. In order to support this on the server, additional configuration is required:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure JWT Bearer Auth to expect our security key
options.TokenValidationParameters =
new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, param) =>
{
return expires > DateTime.UtcNow;
},
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = SecurityKey
};
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
Windows authentication
If Windows authentication is configured in your app, SignalR can use that identity to secure hubs. However, in
order to send messages to individual users, you need to add a custom User ID provider. This is because the
Windows authentication system doesn't provide the "Name Identifier" claim that SignalR uses to determine the
user name.
Add a new class that implements IUserIdProvider and retrieve one of the claims from the user to use as the
identifier. For example, to use the "Name" claim (which is the Windows username in the form [Domain]\[Username]
), create the following class:
Rather than ClaimTypes.Name , you can use any value from the User (such as the Windows SID identifier, etc.).
NOTE
The value you choose must be unique among all the users in your system. Otherwise, a message intended for one user could
end up going to a different user.
Register this component in your Startup.ConfigureServices method after the call to .AddSignalR
services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}
In the .NET Client, Windows Authentication must be enabled by setting the UseDefaultCredentials property:
[Authorize]
public class ChatHub: Hub
{
}
You can use the constructor arguments and properties of the [Authorize] attribute to restrict access to only users
matching specific authorization policies. For example, if you have a custom authorization policy called
MyAuthorizationPolicy you can ensure that only users matching that policy can access the hub using the following
code:
[Authorize("MyAuthorizationPolicy")]
public class ChatHub: Hub
{
}
Individual hub methods can have the [Authorize] attribute applied as well. If the current user doesn't match the
policy applied to the method, an error is returned to the caller:
[Authorize]
public class ChatHub: Hub
{
public async Task Send(string message)
{
// ... send a message to all users ...
}
[Authorize("Administrators")]
public void BanUser(string userName)
{
// ... ban a user from the chat room (something only Administrators can do) ...
}
}
Security considerations in ASP.NET Core SignalR
7/16/2018 • 3 minutes to read • Edit Online
By Andrew Stanton-Nurse
Overview
SignalR provides a number of security protections by default. It's important to understand how to configure these
protections.
Cross-origin resource sharing
Cross-origin resource sharing (CORS ) can be used to allow cross-origin SignalR connections in the browser. If
your JavaScript code is hosted on a different domain name from your SignalR app, you have to enable the
ASP.NET Core CORS middleware in order to allow the connection. In general, allow cross-origin requests only
from domains you control. For example, if your site is hosted on http://www.example.com and your SignalR app is
hosted on http://signalr.example.com , you should configure CORS in your SignalR app to only allow the origin
www.example.com .
For more information on configuring CORS, see the documentation on ASP.NET Core CORS. SignalR requires
the following CORS policies in order to operate correctly:
The policy must allow the specific origins you expect, or allow any origin (not recommended).
HTTP methods GET and POST must be allowed.
Credentials must be enabled, even when you aren't using authentication.
For example, the following CORS policy allows a SignalR browser client hosted on http://example.com to access
your SignalR app:
app.UseSignalR();
NOTE
SignalR is not compatible with the built-in CORS feature in Azure App Service.
ApplicationMaxBufferSize represents the maximum number of bytes from the client that the server buffers. If
the client attempts to send a message larger than this limit, the connection may be closed.
TransportMaxBufferSize represents the maximum number of bytes the server can send. If the server attempts
to send a message (includes return values from hub methods) larger than this limit, an exception will be thrown.
Setting the limit to 0 disables the limit entirely. However, this should be done with extreme caution. Removing the
limit allows a client to send a message of any size. This could be used by a malicious client to cause excess memory
to be allocated, which could dramatically reduce the number of concurrent connections your app can support.
Use MessagePack Hub Protocol in SignalR for
ASP.NET Core
7/16/2018 • 2 minutes to read • Edit Online
By Brennan Conroy
This article assumes the reader is familiar with the topics covered in Get Started.
What is MessagePack?
MessagePack is a binary serialization format that is fast and compact. It's useful when performance and
bandwidth are a concern because it creates smaller messages compared to JSON. Because it's a binary format,
messages are unreadable when looking at network traces and logs unless the bytes are passed through a
MessagePack parser. SignalR has built-in support for the MessagePack format, and provides APIs for the client
and server to use.
NOTE
JSON is enabled by default. Adding MessagePack enables support for both JSON and MessagePack clients.
services.AddSignalR()
.AddMessagePackProtocol();
To customize how MessagePack will format your data, AddMessagePackProtocol takes a delegate for configuring
options. In that delegate, the FormatterResolvers property can be used to configure MessagePack serialization
options. For more information on how the resolvers work, visit the MessagePack library at MessagePack-CSharp.
Attributes can be used on the objects you want to serialize to define how they should be handled.
services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
{
MessagePack.Resolvers.StandardResolver.Instance
};
});
NOTE
This AddMessagePackProtocol call takes a delegate for configuring options just like the server.
JavaScript client
MessagePack support for the Javascript client is provided by the @aspnet/signalr-protocol-msgpack NPM package.
After installing the npm package, the module can be used directly via a JavaScript module loader or imported into
the browser by referencing the node_modules\@aspnet\signalr-protocol-msgpack\dist\browser\signalr-protocol-
msgpack.js file. In a browser the msgpack5 library must also be referenced. Use a <script> tag to create a
reference. The library can be found at node_modules\msgpack5\dist\msgpack5.js.
NOTE
When using the <script> element, the order is important. If signalr-protocol-msgpack.js is referenced before
msgpack5.js, an error occurs when trying to connect with MessagePack. signalr.js is also required before signalr-protocol-
msgpack.js.
<script src="~/lib/signalr/signalr.js"></script>
<script src="~/lib/msgpack5/msgpack5.js"></script>
<script src="~/lib/signalr/signalr-protocol-msgpack.js"></script>
NOTE
At this time, there are no configuration options for the MessagePack protocol on the JavaScript client.
Related resources
Get Started
.NET client
JavaScript client
Use streaming in ASP.NET Core SignalR
7/16/2018 • 2 minutes to read • Edit Online
By Brennan Conroy
ASP.NET Core SignalR supports streaming return values of server methods. This is useful for scenarios where
fragments of data will come in over time. When a return value is streamed to the client, each fragment is sent to
the client as soon as it becomes available, rather than waiting for all the data to become available.
View or download sample code (how to download)
NOTE
Write to the ChannelReader on a background thread and return the ChannelReader as soon as possible. Other hub
invocations will be blocked until a ChannelReader is returned.
return channel.Reader;
}
writer.TryComplete();
}
}
.NET client
The StreamAsChannelAsync method on HubConnection is used to invoke a streaming method. Pass the hub method
name, and arguments defined in the hub method to StreamAsChannelAsync . The generic parameter on
StreamAsChannelAsync<T> specifies the type of objects returned by the streaming method. A ChannelReader<T> is
returned from the stream invocation, and represents the stream on the client. To read data, a common pattern is to
loop over WaitToReadAsync and call TryRead when data is available. The loop will end when the stream has been
closed by the server, or the cancellation token passed to StreamAsChannelAsync is canceled.
Console.WriteLine("Streaming completed");
JavaScript client
JavaScript clients call streaming methods on hubs by using connection.stream . The stream method accepts two
arguments:
The name of the hub method. In the following example, the hub method name is Counter .
Arguments defined in the hub method. In the following example, the arguments are: a count for the number of
stream items to receive, and the delay between stream items.
connection.stream returns an IStreamResult which contains a subscribe method. Pass an IStreamSubscriber to
subscribe and set the next , error , and complete callbacks to get notifications from the stream invocation.
To end the stream from the client call the dispose method on the ISubscription that is returned from the
subscribe method.
Related resources
Hubs
.NET client
JavaScript client
Publish to Azure
Differences between ASP.NET SignalR and ASP.NET
Core SignalR
9/10/2018 • 3 minutes to read • Edit Online
ASP.NET Core SignalR isn't compatible with clients or servers for ASP.NET SignalR. This article details features
which have been removed or changed in ASP.NET Core SignalR.
Supported Server Platforms .NET Framework 4.5 or later .NET Framework 4.6.1 or later
.NET Core 2.1 or later
Feature differences
Automatic reconnects
Automatic reconnects are no longer supported. Previously, SignalR tried to reconnect to the server if the
connection was dropped. Now, if the client is disconnected the user must explicitly start a new connection if they
want to reconnect.
Protocol support
ASP.NET Core SignalR supports JSON, as well as a new binary protocol based on MessagePack. Additionally,
custom protocols can be created.
services.AddSignalR()
To configure routing, map routes to hubs inside the UseSignalR method call in the Startup.Configure method.
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/hub");
});
npm init -y
npm install @aspnet/signalr
jQuery
The dependency on jQuery has been removed, however projects can still use jQuery.
JavaScript client method syntax
The JavaScript syntax has changed from the previous version of SignalR. Rather than using the $connection
object, create a connection using the HubConnectionBuilder API.
Use the on method to specify client methods that the hub can call.
connection.on("ReceiveMessage", (user, message) => {
const msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
const encodedMsg = user + " says " + msg;
log(encodedMsg);
});
After creating the client method, start the hub connection. Chain a catch method to log or handle errors.
Hub proxies
Hub proxies are no longer automatically generated. Instead, the method name is passed into the invoke API as a
string.
.NET and other clients
The Microsoft.AspNetCore.SignalR.Client NuGet package contains the .NET client libraries for ASP.NET Core
SignalR.
Use the HubConnectionBuilder to create and build an instance of a connection to a hub.
Scaleout differences
ASP.NET SignalR supports both SQL Server and Redis. ASP.NET Core SignalR supports both Azure SignalR
Service and Redis.
ASP.NET
SignalR scaleout with Azure Service Bus
SignalR scaleout with Redis
SignalR scaleout with SQL Server
ASP.NET Core
Azure SignalR Service
Additional resources
Hubs
JavaScript client
.NET client
Supported platforms
Test, debug, and troubleshoot in ASP.NET Core
7/3/2018 • 2 minutes to read • Edit Online
Test
Unit Testing in .NET Core and .NET Standard
See how to use unit testing in .NET Core and .NET Standard projects.
Integration tests
Learn how integration tests ensure that an app's components function correctly at the infrastructure level,
including the database, file system, and network.
Razor Pages unit tests
Discover how to create unit tests for Razor Pages apps.
Test controllers
Learn how to test controller logic in ASP.NET Core with Moq and xUnit.
Debug
Learn to debug using Visual Studio
Discover the features of the Visual Studio debugger in a step-by-step walkthrough.
Debugging with Visual Studio Code
Find out about the debugging support built into Visual Studio Code.
Debug ASP.NET Core 2.x source
Learn how to debug .NET Core and ASP.NET Core sources.
Remote debugging
Explore how to set up and configure a Visual Studio 2017 ASP.NET Core app, deploy it to IIS using Azure, and
attach the remote debugger from Visual Studio.
Snapshot debugging
Find out how to collect snapshots on your top-throwing exceptions so that you have the information you need to
diagnose issues in production.
Troubleshoot
Troubleshoot
Understand and troubleshoot warnings and errors with ASP.NET Core projects.
Integration tests in ASP.NET Core
8/16/2018 • 17 minutes to read • Edit Online
NOTE
For testing SPAs, we recommended a tool such as Selenium, which can automate a browser.
NOTE
In discussions of integration tests, the tested project is frequently called the system under test, or "SUT" for short.
The unit tests documentation describes how to set up a test project and test runner, along with detailed
instructions on how to run tests and recommendations for how to name tests and test classes.
NOTE
When creating a test project for an app, separate the unit tests from the integration tests into different projects. This helps
ensure that infrastructure testing components aren't accidently included in the unit tests. Separation of unit and
integration tests also allows control over which set of tests are run.
There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only
difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after
the page model class (for example, IndexPageTests to test component integration for the Index page). In an MVC
app, tests are usually organized by controller classes and named after the controllers they test (for example,
HomeControllerTests to test component integration for the Home controller ).
CreateClient creates an instance of HttpClient that automatically follows redirects and handles cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
By disallowing the client to follow the redirect, the following checks can be made:
The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not
the final status code after the redirect to the Login page, which would be HttpStatusCode.OK.
The Location header value in the response headers is checked to confirm that it starts with
http://localhost/Identity/Account/Login , not the final Login page response, where the Location header
wouldn't be present.
For more information on WebApplicationFactoryClientOptions , see the Client options section.
Customize WebApplicationFactory
Web host configuration can be created independently of the test classes by inheriting from
WebApplicationFactory to create one or more custom factories:
1. Inherit from WebApplicationFactory and override ConfigureWebHost. The IWebHostBuilder allows the
configuration of the service collection with ConfigureServices:
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<RazorPagesProject.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
try
{
// Seed the database with test data.
Utilities.InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}
Database seeding in the sample app is performed by the InitializeDbForTests method. The method is
described in the Integration tests sample: Test app organization section.
2. Use the custom CustomWebApplicationFactory in test classes. The following example uses the factory in the
IndexPageTests class:
public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> _factory;
public IndexPageTests(
CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
The sample app's client is configured to prevent the HttpClient from following redirects. As explained in
the Test a secure endpoint section, this permits tests to check the result of the app's first response. The
first response is a redirect in many of these tests with a Location header.
3. A typical test uses the HttpClient and helper methods to process the request and the response:
[Fact]
public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
{
// Arrange
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data
protection antiforgery system. In order to arrange for a test's POST request, the test app must:
1. Make a request for the page.
2. Parse the antiforgery cookie and request validation token from the response.
3. Make the POST request with the antiforgery cookie and request validation token in place.
The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper
method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check
with the following methods:
GetDocumentAsync – Receives the HttpResponseMessage and returns an IHtmlDocument . GetDocumentAsync
uses a factory that prepares a virtual response based on the original HttpResponseMessage . For more
information, see the AngleSharp documentation.
SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call
SendAsync(HttpRequestMessage) to submit requests to the SUT. Overloads for SendAsync accept the HTML
form ( IHtmlFormElement ) and the following:
Submit button of the form ( IHtmlElement )
Form values collection ( IEnumerable<KeyValuePair<string, string>> )
Submit button ( IHtmlElement ) and form values ( IEnumerable<KeyValuePair<string, string>> )
NOTE
AngleSharp is a third-party parsing library used for demonstration purposes in this topic and the sample app. AngleSharp
isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the Html
Agility Pack (HAP). Another approach is to write code to handle the antiforgery system's request verification token and
antiforgery cookie directly.
The Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of
WithWebHostBuilder . This test performs a record delete in the database by triggering a form submission in the
SUT.
Because another test in the IndexPageTests class performs an operation that deletes all of the records in the
database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is
seeded in this test method to ensure that a record is present for the SUT to delete. Selecting the deleteBtn1
button of the messages form in the SUT is simulated in the request to the SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
try
{
Utilities.InitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: " +
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("button[id='deleteBtn1']"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Client options
The following table shows the default WebApplicationFactoryClientOptions available when creating HttpClient
instances.
Create the WebApplicationFactoryClientOptions class and pass it to the CreateClient method (default values are
shown in the code example):
_client = _factory.CreateClient(clientOptions);
Services/QuoteService.cs:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Startup.cs:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
[BindProperty]
public Message Message { get; set; }
[TempData]
public string MessageAnalysisResult { get; set; }
Pages/Index.cs:
To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test.
The mock service replaces the app's QuoteService with a service provided by the test app, called
TestQuoteService :
IntegrationTests.IndexPageTests.cs:
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
The markup produced during the test's execution reflects the quote text supplied by TestQuoteService , thus the
assertion passes:
How the test infrastructure infers the app content root path
The WebApplicationFactory constructor infers the app content root path by searching for a
WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal
to the TEntryPoint assembly System.Reflection.Assembly.FullName . In case an attribute with the correct key isn't
found, WebApplicationFactory falls back to searching for a solution file (*.sln) and appends the TEntryPoint
assembly name to the solution directory. The app root directory (the content root path) is used to discover views
and content files.
In most cases, it isn't necessary to explicitly set the app content root, as the search logic usually finds the correct
content root at runtime. In special scenarios where the content root isn't found using the built-in search
algorithm, the app content root can be specified explicitly or by using custom logic. To set the app content root in
those scenarios, call the UseSolutionRelativeContentRoot extension method from the
Microsoft.AspNetCore.TestHost package. Supply the solution's relative path and optional solution file name or
glob pattern (default = *.sln ).
Call the UseSolutionRelativeContentRoot extension method using ONE of the following approaches:
When configuring test classes with WebApplicationFactory , provide a custom configuration with the
IWebHostBuilder:
public IndexPageTests(
WebApplicationFactory<RazorPagesProject.Startup> factory)
{
var _factory = factory.WithWebHostBuilder(builder =>
{
builder.UseSolutionRelativeContentRoot("<SOLUTION-RELATIVE-PATH>");
...
});
}
When configuring test classes with a custom WebApplicationFactory , inherit from WebApplicationFactory
and override ConfigureWebHost:
...
});
}
}
{
"shadowCopy": false
}
Message app (the SUT) src/RazorPagesProject Allows a user to add, delete one, delete
all, and analyze messages.
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code
or the command line, execute the following command at a command prompt in the
tests/RazorPagesProject.Tests folder:
dotnet test
The test framework is xUnit. Integration tests are conducted using the Microsoft.AspNetCore.TestHost, which
includes the TestServer. Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test
host and test server, the TestHost and TestServer packages don't require direct package references in the test
app's project file or developer configuration in the test app.
Seeding the database for testing
Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete
test calls for a database record deletion, so the database must have at least one record for the delete request to
succeed.
The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:
Additional resources
Unit tests
Razor Pages unit tests
Middleware
Test controllers
Razor Pages unit tests in ASP.NET Core
8/16/2018 • 9 minutes to read • Edit Online
By Luke Latham
ASP.NET Core supports unit tests of Razor Pages apps. Tests of the data access layer (DAL ) and page models
help ensure:
Parts of a Razor Pages app work independently and together as a unit during app construction.
Classes and methods have limited scopes of responsibility.
Additional documentation exists on how the app should behave.
Regressions, which are errors brought about by updates to the code, are found during automated building
and deployment.
This topic assumes that you have a basic understanding of Razor Pages apps and unit tests. If you're unfamiliar
with Razor Pages apps or test concepts, see the following topics:
Introduction to Razor Pages
Get started with Razor Pages
Unit testing C# in .NET Core using dotnet test and xUnit
View or download sample code (how to download)
The sample project is composed of two apps:
The tests can be run using the built-in test features of an IDE, such as Visual Studio. If using Visual Studio Code
or the command line, execute the following command at a command prompt in the
tests/RazorPagesTestSample.Tests folder:
dotnet test
Unit tests of the DAL require DbContextOptions when creating a new AppDbContext for each test. One approach
to creating the DbContextOptions for each test is to use a DbContextOptionsBuilder:
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
The problem with this approach is that each test receives the database in whatever state the previous test left it.
This can be problematic when trying to write atomic unit tests that don't interfere with each other. To force the
AppDbContext to use a new database context for each test, supply a DbContextOptions instance that's based on a
new service provider. The test app shows how to do this using its Utilities class method
TestingDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs):
return builder.Options;
}
Using the DbContextOptions in the DAL unit tests allows each test to run atomically with a fresh database
instance:
Each test method in the DataAccessLayerTest class (UnitTests/DataAccessLayerTest.cs) follows a similar Arrange-
Act-Assert pattern:
1. Arrange: The database is configured for the test and/or the expected outcome is defined.
2. Act: The test is executed.
3. Assert: Assertions are made to determine if the test result is a success.
For example, the DeleteMessageAsync method is responsible for removing a single message identified by its Id
(src/RazorPagesTestSample/Data/AppDbContext.cs):
public async virtual Task DeleteMessageAsync(int id)
{
var message = await Messages.FindAsync(id);
if (message != null)
{
Messages.Remove(message);
await SaveChangesAsync();
}
}
There are two tests for this method. One test checks that the method deletes a message when the message is
present in the database. The other method tests that the database doesn't change if the message Id for deletion
doesn't exist. The DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound method is shown below:
[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
// Act
await db.DeleteMessageAsync(recId);
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(x => x.Id),
actualMessages.OrderBy(x => x.Id),
new Utilities.MessageComparer());
}
}
First, the method performs the Arrange step, where preparation for the Act step takes place. The seeding
messages are obtained and held in seedMessages . The seeding messages are saved into the database. The
message with an Id of 1 is set for deletion. When the DeleteMessageAsync method is executed, the expected
messages should have all of the messages except for the one with an Id of 1 . The expectedMessages variable
represents this expected outcome.
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
The method acts: The DeleteMessageAsync method is executed passing in the recId of 1 :
// Act
await db.DeleteMessageAsync(recId);
Finally, the method obtains the Messages from the context and compares it to the expectedMessages asserting
that the two are equal:
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
// Arrange
var expectedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(expectedMessages);
await db.SaveChangesAsync();
var recId = 4;
// Act
await db.DeleteMessageAsync(recId);
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
OnGetAsync Obtains the messages from the DAL for the UI using the
GetMessagesAsync method.
The page model methods are tested using seven tests in the IndexPageTests class
(tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). The tests use the familiar Arrange-Assert-Act
pattern. These tests focus on:
Determining if the methods follow the correct behavior when the ModelState is invalid.
Confirming the methods produce the correct IActionResult .
Checking that property value assignments are made correctly.
This group of tests often mock the methods of the DAL to produce expected data for the Act step where a page
model method is executed. For example, the GetMessagesAsync method of the AppDbContext is mocked to
produce output. When a page model method executes this method, the mock returns the result. The data doesn't
come from the database. This creates predictable, reliable test conditions for using the DAL in the page model
tests.
The OnGetAsync_PopulatesThePageModel_WithAListOfMessages test shows how the GetMessagesAsync method is
mocked for the page model:
When the OnGetAsync method is executed in the Act step, it calls the page model's GetMessagesAsync method.
Unit test Act step (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):
// Act
await pageModel.OnGetAsync();
The GetMessagesAsync method in the DAL doesn't return the result for this method call. The mocked version of
the method returns the result.
In the Assert step, the actual messages ( actualMessages ) are assigned from the Messages property of the page
model. A type check is also performed when the messages are assigned. The expected and actual messages are
compared by their Text properties. The test asserts that the two List<Message> instances contain the same
messages.
// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
Other tests in this group create page model objects that include the DefaultHttpContext , the
ModelStateDictionary , an ActionContext to establish the PageContext , a ViewDataDictionary , and a PageContext .
These are useful in conducting tests. For example, the message app establishes a ModelState error with
AddModelError to check that a valid PageResult is returned when OnPostAddMessageAsync is executed:
[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
// Arrange
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("InMemoryDb");
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var httpContext = new DefaultHttpContext();
var modelState = new ModelStateDictionary();
var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(),
modelState);
var modelMetadataProvider = new EmptyModelMetadataProvider();
var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
var pageContext = new PageContext(actionContext)
{
ViewData = viewData
};
var pageModel = new IndexModel(mockAppDbContext.Object)
{
PageContext = pageContext,
TempData = tempData,
Url = new UrlHelper(actionContext)
};
pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");
// Act
var result = await pageModel.OnPostAddMessageAsync();
// Assert
Assert.IsType<PageResult>(result);
}
Additional resources
Unit testing C# in .NET Core using dotnet test and xUnit
Test controllers
Unit Test Your Code (Visual Studio)
Integration tests
xUnit.net
Getting started with xUnit.net (.NET Core/ASP.NET Core)
Moq
Moq Quickstart
Test controller logic in ASP.NET Core
9/18/2018 • 12 minutes to read • Edit Online
By Steve Smith
Controllers play a central role in any ASP.NET Core MVC app. As such, you should have confidence that
controllers behave as intended. Automated tests can detect errors before the app is deployed to a production
environment.
View or download sample code (how to download)
return View(model);
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
The Home controller's HTTP POST Index method tests verifies that:
When ModelState.IsValid is false , the action method returns a 400 Bad Request ViewResult with the
appropriate data.
When ModelState.IsValid is true :
The Add method on the repository is called.
A RedirectToActionResult is returned with the correct arguments.
An invalid model state is tested by adding errors using AddModelError as shown in the first test below:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
When ModelState isn't valid, the same ViewResult is returned as for a GET request. The test doesn't attempt to
pass in an invalid model. Passing an invalid model isn't a valid approach, since model binding isn't running
(although an integration test does use model binding). In this case, model binding isn't tested. These unit tests
are only testing the code in the action method.
The second test verifies that when the ModelState is valid:
A new BrainstormSession is added (via the repository).
The method returns a RedirectToActionResult with the expected properties.
Mocked calls that aren't called are normally ignored, but calling Verifiable at the end of the setup call allows
mock validation in the test. This is performed with the call to mockRepo.Verify , which fails the test if the expected
method wasn't called.
NOTE
The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also
called "loose" mocks or stubs). Learn more about customizing Mock behavior with Moq.
Another controller in the sample app displays information related to a particular brainstorming session. The
controller includes logic to deal with invalid id values (there are two return scenarios in the following example
to cover these scenarios). The final return statement returns a new StormSessionViewModel to the view:
return View(viewModel);
}
}
The unit tests include one test for each return scenario in the Session controller Index action:
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}
Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas route:
A list of ideas ( IdeaDTO ) associated with a brainstorming session is returned by the ForSession method.
The Create method adds new ideas to a session.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Avoid returning business domain entities directly via API calls. Domain entities:
Often include more data than the client requires.
Unnecessarily couple the app's internal domain model with the publicly exposed API.
Mapping between domain entities and the types returned to the client can be performed:
Manually with a LINQ Select , as the sample app uses. For more information, see LINQ (Language
Integrated Query).
Automatically with a library, such as AutoMapper.
Next, the sample app demonstrates unit tests for the Create and ForSession API methods of the Ideas
controller.
The sample app contains two ForSession tests. The first test determines if ForSession returns a
NotFoundObjectResult (HTTP Not Found) for an invalid session:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
The second ForSession test determines if ForSession returns a list of session ideas ( <List<IdeaDTO>> ) for a valid
session. The checks also examine the first idea to confirm its Name property is correct:
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
To test the behavior of the Create method when the ModelState is invalid, the sample app adds a model error to
the controller as part of the test. Don't try to test model validation or model binding in unit tests—just test the
action method's behavior when confronted with an invalid ModelState :
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.Create(model: null);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
The second test of Create depends on the repository returning null , so the mock repository is configured to
return null . There's no need to create a test database (in memory or otherwise) and construct a query that
returns this result. The test can be accomplished in a single statement, as the sample code illustrates:
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
The third Create test, Create_ReturnsNewlyCreatedIdeaForSession , verifies that the repository's UpdateAsync
method is called. The mock is called with Verifiable , and the mocked repository's Verify method is called to
confirm the verifiable method is executed. It's not the unit test's responsibility to ensure that the UpdateAsync
method saved the data—that can be performed with an integration test.
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(newIdea);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Test ActionResult<T>
In ASP.NET Core 2.1 or later, ActionResult<T> (ActionResult<TValue>) enables you to return a type deriving
from ActionResult or return a specific type.
The sample app includes a method that returns a List<IdeaDTO> for a given session id . If the session id
doesn't exist, the controller returns NotFound:
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
return result;
}
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;
// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
For for a valid session id , the second test confirms that the method returns:
An ActionResult with a List<IdeaDTO> type.
The ActionResult<T>.Value is a List<IdeaDTO> type.
The first item in the list is a valid idea matching the idea stored in the mock session (obtained by calling
GetTestSession ).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSessionActionResult(testSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
The sample app also includes a method to create a new Idea for a given session. The controller returns:
BadRequest for an invalid model.
NotFound if the session doesn't exist.
CreatedAtAction when the session is updated with the new idea.
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (session == null)
{
return NotFound(model.SessionId);
}
await _sessionRepository.UpdateAsync(session);
// Act
var result = await controller.CreateActionResult(model: null);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
The second test checks that a NotFound is returned if the session doesn't exist.
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Additional resources
Test, debug, and troubleshoot in ASP.NET Core
Integration tests in ASP.NET Core
Create and run unit tests with Visual Studio.
Repository pattern with ASP.NET Core
Explicit Dependencies Principle
Troubleshoot ASP.NET Core projects
7/10/2018 • 2 minutes to read • Edit Online
By Rick Anderson
The following links provide troubleshooting guidance:
Troubleshoot ASP.NET Core on Azure App Service
Troubleshoot ASP.NET Core on IIS
Common errors reference for Azure App Service and IIS with ASP.NET Core
NDC Conference (London, 2018): Diagnosing issues in ASP.NET Core Applications
ASP.NET Blog: Troubleshooting ASP.NET Core Performance Problems
Both 32 and 64 bit versions of the .NET Core SDK are installed. Only templates from the 64 bit version(s)
installed at 'C:\Program Files\dotnet\sdk\' will be displayed.
This warning appears when both 32-bit (x86) and 64-bit (x64) versions of the .NET Core SDK are installed.
Common reasons both versions may be installed include:
You originally downloaded the .NET Core SDK installer using a 32-bit machine but then copied it across and
installed it on a 64-bit machine.
The 32-bit .NET Core SDK was installed by another application.
The wrong version was downloaded and installed.
Uninstall the 32-bit .NET Core SDK to prevent this warning. Uninstall from Control Panel > Programs and
Features > Uninstall or change a program. If you understand why the warning occurs and its implications, you
can ignore the warning.
The .NET Core SDK is installed in multiple locations
In the New Project dialog for ASP.NET Core, you may see the following warning:
The .NET Core SDK is installed in multiple locations. Only templates from the SDK(s) installed at 'C:\Program
Files\dotnet\sdk\' will be displayed.
You see this message when you have at least one installation of the .NET Core SDK in a directory outside of
C:\Program Files\dotnet\sdk\. Usually this happens when the .NET Core SDK has been deployed on a machine
using copy/paste instead of the MSI installer.
Uninstall the 32-bit .NET Core SDK to prevent this warning. Uninstall from Control Panel > Programs and
Features > Uninstall or change a program. If you understand why the warning occurs and its implications, you
can ignore the warning.
No .NET Core SDKs were detected
In the New Project dialog for ASP.NET Core, you may see the following warning:
No .NET Core SDKs were detected, ensure they are included in the environment variable 'PATH'.
This warning appears when the environment variable PATH doesn't point to any .NET Core SDKs on the machine.
To resolve this problem:
Install or verify the .NET Core SDK is installed.
Verify that the PATH environment variable points to the location in which the SDK is installed. The installer
normally sets the PATH .
Work with data in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Get started with Razor Pages and Entity Framework Core using Visual Studio
Get started with Razor Pages and EF
Create, Read, Update, and Delete operations
Sort, filter, page, and group
Migrations
Create a complex data model
Read related data
Update related data
Handle concurrency conflicts
Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
Get started
Create, Read, Update, and Delete operations
Sort, filter, page, and group
Migrations
Create a complex data model
Read related data
Update related data
Handle concurrency conflicts
Inheritance
Advanced topics
ASP.NET Core with EF Core - new database (Entity Framework Core documentation site)
ASP.NET Core with EF Core - existing database (Entity Framework Core documentation site)
Get started with ASP.NET Core and Entity Framework 6
Azure Storage
Add Azure Storage by using Visual Studio Connected Services
Get started with Azure Blob storage and Visual Studio Connected Services
Get started with Queue Storage and Visual Studio Connected Services
Get started with Azure Table Storage and Visual Studio Connected Services
Razor Pages with Entity Framework Core in
ASP.NET Core - Tutorial 1 of 8
9/18/2018 • 15 minutes to read • Edit Online
The ASP.NET Core 2.0 version of this tutorial can be found in this PDF file.
The ASP.NET Core 2.1 version of this tutorial has many improvements over the 2.0 version.
By Tom Dykstra and Rick Anderson
The Contoso University sample web app demonstrates how to create an ASP.NET Core Razor Pages
app using Entity Framework (EF ) Core.
The sample app is a web site for a fictional Contoso University. It includes functionality such as student
admission, course creation, and instructor assignments. This page is the first in a series of tutorials that
explain how to build the Contoso University sample app.
Download or view the completed app. Download instructions.
Prerequisites
Visual Studio
.NET Core CLI
Visual Studio 2017 version 15.7.3 or later with the following workloads:
ASP.NET and web development
.NET Core cross-platform development
.NET Core 2.1 SDK or later
Familiarity with Razor Pages. New programmers should complete Get started with Razor Pages before
starting this series.
Troubleshooting
If you run into a problem you can't resolve, you can generally find the solution by comparing your code
to the completed project. A good way to get help is by posting a question to StackOverflow.com for
ASP.NET Core or EF Core.
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-
test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Students/Index">Students</a></li>
<li><a asp-page="/Courses/Index">Courses</a></li>
<li><a asp-page="/Instructors/Index">Instructors</a></li>
<li><a asp-page="/Departments/Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
In Pages/Index.cshtml, replace the contents of the file with the following code to replace the text about
ASP.NET and MVC with text about this app:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core Razor Pages web app.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series of tutorials.</p>
<p>
<a class="btn btn-default"
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro">
See the tutorial »
</a>
</p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p>
<a class="btn btn-default"
href="https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-
rp/intro/samples/cu-final">
See project source code »
</a>
</p>
</div>
</div>
There's a one-to-many relationship between Student and Enrollment entities. There's a one-to-many
relationship between Course and Enrollment entities. A student can enroll in any number of courses. A
course can have any number of students enrolled in it.
In the following sections, a class for each one of these entities is created.
The Student entity
Create a Models folder. In the Models folder, create a class file named Student.cs with the following code:
using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
The ID property becomes the primary key column of the database (DB ) table that corresponds to this
class. By default, EF Core interprets a property that's named ID or classnameID as the primary key. In
classnameID , classname is the name of the class. The alternative automatically recognized primary key
is StudentID in the preceding example.
The Enrollments property is a navigation property. Navigation properties link to other entities that are
related to this entity. In this case, the Enrollments property of a Student entity holds all of the
Enrollment entities that are related to that Student . For example, if a Student row in the DB has two
related Enrollment rows, the Enrollments navigation property contains those two Enrollment entities. A
related Enrollment row is a row that contains that student's primary key value in the StudentID column.
For example, suppose the student with ID=1 has two rows in the Enrollment table. The Enrollment
table has two rows with StudentID = 1. StudentID is a foreign key in the Enrollment table that specifies
the student in the Student table.
If a navigation property can hold multiple entities, the navigation property must be a list type, such as
ICollection<T> . ICollection<T> can be specified, or a type such as List<T> or HashSet<T> . When
ICollection<T> is used, EF Core creates a HashSet<T> collection by default. Navigation properties that
hold multiple entities come from many-to-many and one-to-many relationships.
The Enrollment entity
In the Models folder, create Enrollment.cs with the following code:
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
like the Student entity. Typically developers choose one pattern and use it throughout the data model.
In a later tutorial, using ID without classname is shown to make it easier to implement inheritance in the
data model.
The property is an enum . The question mark after the Grade type declaration indicates that the
Grade
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 StudentIDproperty is a foreign key, and the corresponding navigation property is Student . An
Enrollment entity is associated with one Student entity, so the property contains a single Student
entity. The Student entity differs from the Student.Enrollments navigation property, which contains
multiple Enrollment entities.
The CourseID property is a foreign key, and the corresponding navigation property is Course . An
Enrollment entity is associated with one Course entity.
EF Core interprets a property as a foreign key if it's named
<navigation property name><primary key property name> . For example, StudentID for the Student
navigation property, since the Student entity's primary key is ID . Foreign key properties can also be
named <primary key property name> . For example, CourseID since the Course entity's primary key is
CourseID .
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
The Enrollments property is a navigation property. A Course entity can be related to any number of
Enrollment entities.
The DatabaseGenerated attribute allows the app to specify the primary key rather than having the DB
generate it.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}
The name of the connection string is passed in to the context by calling a method on a
DbContextOptions object. For local development, the ASP.NET Core configuration system reads the
connection string from the appsettings.json file.
Update main
In Program.cs, modify the Main method to do the following:
Get a DB context instance from the dependency injection container.
Call the EnsureCreated.
Dispose the context when the EnsureCreated method completes.
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
try
{
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
EnsureCreated ensures that the database for the context exists. If it exists, no action is taken. If it does
not exist, then the database and all its schema are created. EnsureCreated does not use migrations to
create the database. A database that is created with EnsureCreated cannot be later updated using
migrations.
EnsureCreated is called on app start, which allows the following work flow:
Delete the DB.
Change the DB schema (for example, add an EmailAddress field).
Run the app.
EnsureCreated creates a DB with the EmailAddress column.
EnsureCreated is convenient early in development when the schema is rapidly evolving. Later in the
tutorial the DB is deleted and migrations are used.
Test the app
Run the app and accept the cookie policy. This app doesn't keep personal information. You can read
about the cookie policy at EU General Data Protection Regulation (GDPR ) support.
Select the Students link and then Create New.
Test the Edit, Details, and Delete links.
Examine the SchoolContext DB context
The main class that coordinates EF Core functionality for a given data model is the DB context class. The
data context is derived from Microsoft.EntityFrameworkCore.DbContext. The data context specifies
which entities are included in the data model. In this project, the class is named SchoolContext .
Update SchoolContext.cs with the following code:
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options)
: base(options)
{
}
The highlighted code creates a DbSet<TEntity> property for each entity set. In EF Core terminology:
An entity set typically corresponds to a DB table.
An entity corresponds to a row in the table.
DbSet<Enrollment> and DbSet<Course> could be omitted. EF Core includes them implicitly because the
Student entity references the Enrollment entity, and the Enrollment entity references the Course
entity. For this tutorial, keep DbSet<Enrollment> and DbSet<Course> in the SchoolContext .
SQL Server Express LocalDB
The connection string specifies SQL Server LocalDB. LocalDB is a lightweight version of the SQL Server
Express Database Engine and is intended for app development, not production use. LocalDB starts on
demand and runs in user mode, so there's no complex configuration. By default, LocalDB creates .mdf
DB files in the C:/Users/<user> directory.
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// context.Database.EnsureCreated();
try
{
var context = services.GetRequiredService<SchoolContext>();
// using ContosoUniversity.Data;
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
Delete any student records and restart the app. If the DB is not initialized, set a break point in
Initialize to diagnose the problem.
View the DB
Open SQL Server Object Explorer (SSOX) from the View menu in Visual Studio. In SSOX, click
(localdb)\MSSQLLocalDB > Databases > ContosoUniversity1.
Expand the Tables node.
Right-click the Student table and click View Data to see the columns created and the rows inserted
into the table.
Asynchronous code
Asynchronous programming is the default mode for ASP.NET Core and EF Core.
A web server has a limited number of threads available, and in high load situations all of the available
threads might be in use. When that happens, the server can't process new requests until the threads are
freed up. With synchronous code, many threads may be tied up while they aren't actually doing any
work because they're waiting for I/O to complete. With asynchronous code, when a process is waiting
for I/O to complete, its thread is freed up for the server to use for processing other requests. As a result,
asynchronous code enables server resources to be used more efficiently, and the server is enabled to
handle more traffic without delays.
Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations,
the performance hit is negligible, while for high traffic situations, the potential performance
improvement is substantial.
In the following code, the async keyword, Task<T> return value, await keyword, and ToListAsync
method make the code execute asynchronously.
NEXT
ASP.NET Core MVC with EF Core - tutorial series
6/21/2018 • 2 minutes to read • Edit Online
This tutorial teaches ASP.NET Core MVC and Entity Framework Core with controllers and views. Razor Pages is a
new alternative in ASP.NET Core 2.0, a page-based programming model that makes building web UI easier and
more productive. We recommend the Razor Pages tutorial over the MVC version. The Razor Pages tutorial:
Is easier to follow.
Provides more EF Core best practices.
Uses more efficient queries.
Is more current with the latest API.
Covers more features.
Is the preferred approach for new application development.
If you choose this tutorial over the Razor Pages version, let us know why in this GitHub issue.
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
Get Started with ASP.NET Core and Entity Framework
6
9/14/2018 • 3 minutes to read • Edit Online
Overview
To use Entity Framework 6, your project has to compile against .NET Framework, as Entity Framework 6 doesn't
support .NET Core. If you need cross-platform features you will need to upgrade to Entity Framework Core.
The recommended way to use Entity Framework 6 in an ASP.NET Core application is to put the EF6 context and
model classes in a class library project that targets the full framework. Add a reference to the class library from the
ASP.NET Core project. See the sample Visual Studio solution with EF6 and ASP.NET Core projects.
You can't put an EF6 context in an ASP.NET Core project because .NET Core projects don't support all of the
functionality that EF6 commands such as Enable-Migrations require.
Regardless of project type in which you locate your EF6 context, only EF6 command-line tools work with an EF6
context. For example, Scaffold-DbContext is only available in Entity Framework Core. If you need to do reverse
engineering of a database into an EF6 model, see Code First to an Existing Database.
<PropertyGroup>
<TargetFramework>net452</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<AssemblyName>MVCCore</AssemblyName>
<OutputType>Exe</OutputType>
<PackageId>MVCCore</PackageId>
</PropertyGroup>
When creating a new project, use the ASP.NET Core Web Application (.NET Framework) template.
In this sample code, the IDbContextFactory implementation passes in a hard-coded connection string. This is the
connection string that the command-line tools will use. You'll want to implement a strategy to ensure that the class
library uses the same connection string that the calling application uses. For example, you could get the value from
an environment variable in both projects.
You can then get an instance of the context in your controllers by using DI. The code is similar to what you'd write
for an EF Core context:
Sample application
For a working sample application, see the sample Visual Studio solution that accompanies this article.
This sample can be created from scratch by the following steps in Visual Studio:
Create a solution.
Add > New Project > Web > ASP.NET Core Web Application
In project template selection dialog, select API and .NET Framework in dropdown
Add > New Project > Windows Desktop > Class Library (.NET Framework)
In Package Manager Console (PMC ) for both projects, run the command
Install-Package Entityframework .
In the class library project, create data model classes and a context class, and an implementation of
IDbContextFactory .
In PMC for the class library project, run the commands Enable-Migrations and Add-Migration Initial . If
you have set the ASP.NET Core project as the startup project, add -StartupProjectName EF6 to these
commands.
In the Core project, add a project reference to the class library project.
In the Core project, in Startup.cs, register the context for DI.
In the Core project, in appsettings.json, add the connection string.
In the Core project, add a controller and view (s) to verify that you can read and write data. (Note that
ASP.NET Core MVC scaffolding won't work with the EF6 context referenced from the class library.)
Summary
This article has provided basic guidance for using Entity Framework 6 in an ASP.NET Core application.
Additional resources
Entity Framework - Code-Based Configuration
Azure Storage in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Guides
Welcome
Welcome to the Azure Development Lifecycle guide for .NET! This guide introduces the basic concepts of building
a development lifecycle around Azure using .NET tools and processes. After finishing this guide, you'll reap the
benefits of a mature DevOps toolchain.
Azure has several interfaces for provisioning and managing resources, such as the Azure portal, Azure CLI, Azure
PowerShell, Azure Cloud Shell, and Visual Studio. This guide takes a minimalist approach and uses the Azure
Cloud Shell whenever possible to reduce the steps required. However, the Azure portal must be used for some
portions.
Prerequisites
The following subscriptions are required:
Azure — If you don't have an account, get a free trial.
Azure DevOps Services — your Azure DevOps subscription and organization is created in Chapter 4.
GitHub — If you don't have an account, sign up for free.
The following tools are required:
Git — A fundamental understanding of Git is recommended for this guide. Review the Git documentation,
specifically git remote and git push.
.NET Core SDK — Version 2.1.300 or later is required to build and run the sample app. If Visual Studio is
installed with the .NET Core cross-platform development workload, the .NET Core SDK is already
installed.
Verify your .NET Core SDK installation. Open a command shell, and run the following command:
dotnet --version
Azure App Service is Azure's web hosting platform. Deploying a web app to Azure App Service can be done
manually or by an automated process. This section of the guide discusses deployment methods that can be
triggered manually or by script using the command line, or triggered manually using Visual Studio.
In this section, you'll accomplish the following tasks:
Download and build the sample app.
Create an Azure App Service Web App using the Azure Cloud Shell.
Deploy the sample app to Azure using Git.
Deploy a change to the app using Visual Studio.
Add a staging slot to the web app.
Deploy an update to the staging slot.
Swap the staging and production slots.
Note: Linux/macOS users should make appropriate changes for paths, e.g., using forward slash ( / ) rather
than back slash ( \ ).
2. Change your working folder to the simple-feed -reader folder that was created.
cd .\simple-feed-reader\SimpleFeedReader
dotnet build
dotnet run
5. Open a browser and navigate to http://localhost:5000 . The app allows you to type or paste a syndication
feed URL and view a list of news items.
6. Once you're satisfied the app is working correctly, shut it down by pressing Ctrl+C in the command shell.
webappname=mywebapp$RANDOM
b. Create a resource group. Resource groups provide a means to aggregate Azure resources to be managed
as a group.
The az command invokes the Azure CLI. The CLI can be run locally, but using it in the Cloud Shell saves
time and configuration.
c. Create an App Service plan in the S1 tier. An App Service plan is a grouping of web apps that share the
same pricing tier. The S1 tier isn't free, but it's required for the staging slots feature.
d. Create the web app resource using the App Service plan in the same resource group.
e. Set the deployment credentials. These deployment credentials apply to all the web apps in your
subscription. Don't use special characters in the user name.
f. Configure the web app to accept deployments from local Git and display the Git deployment URL. Note
this URL for reference later.
echo Git deployment URL: $(az webapp deployment source config-local-git --name $webappname --resource-
group AzureTutorial --query url --output tsv)
g. Display the web app URL. Browse to this URL to see the blank web app. Note this URL for reference
later.
3. Using a command shell on your local machine, navigate to the web app's project folder (for example,
.\simple-feed-reader\SimpleFeedReader ). Execute the following commands to set up Git to push to the
deployment URL:
a. Add the remote URL to the local repository.
b. Push the local master branch to the azure-prod remote's master branch.
You'll be prompted for the deployment credentials you created earlier. Observe the output in the command
shell. Azure builds the ASP.NET Core app remotely.
4. In a browser, navigate to the Web app URL and note the app has been built and deployed. Additional
changes can be committed to the local Git repository with git commit . These changes are pushed to Azure
with the preceding git push command.
Deployment with Visual Studio
Note: This section applies to Windows only. Linux and macOS users should make the change described in step
2 below. Save the file, and commit the change to the local repository with git commit . Finally, push the change
with git push , as in the first section.
The app has already been deployed from the command shell. Let's use Visual Studio's integrated tools to deploy
an update to the app. Behind the scenes, Visual Studio accomplishes the same thing as the command line tooling,
but within Visual Studio's familiar UI.
1. Open SimpleFeedReader.sln in Visual Studio.
2. In Solution Explorer, open Pages\Index.cshtml. Change <h2>Simple Feed Reader</h2> to
<h2>Simple Feed Reader - V2</h2> .
5. Visual Studio can create a new App Service resource, but this update will be published over the existing
deployment. In the Pick a publish target dialog, select App Service from the list on the left, and then
select Select Existing. Click Publish.
6. In the App Service dialog, confirm that the Microsoft or Organizational account used to create your Azure
subscription is displayed in the upper right. If it's not, click the drop-down and add it.
7. Confirm that the correct Azure Subscription is selected. For View, select Resource Group. Expand the
AzureTutorial resource group and then select the existing web app. Click OK.
Visual Studio builds and deploys the app to Azure. Browse to the web app URL. Validate that the <h2> element
modification is live.
Deployment slots
Deployment slots support the staging of changes without impacting the app running in production. Once the
staged version of the app is validated by a quality assurance team, the production and staging slots can be
swapped. The app in staging is promoted to production in this manner. The following steps create a staging slot,
deploy some changes to it, and swap the staging slot with production after verification.
1. Sign in to the Azure Cloud Shell, if not already signed in.
2. Create the staging slot.
a. Create a deployment slot with the name staging.
az webapp deployment slot create --name $webappname --resource-group AzureTutorial --slot staging
b. Configure the staging slot to use deployment from local Git and get the staging deployment URL. Note
this URL for reference later.
echo Git deployment URL for staging: $(az webapp deployment source config-local-git --name $webappname -
-resource-group AzureTutorial --slot staging --query url --output tsv)
c. Display the staging slot's URL. Browse to the URL to see the empty staging slot. Note this URL for
reference later.
3. In a text editor or Visual Studio, modify Pages/Index.cshtml again so that the <h2> element reads
<h2>Simple Feed Reader - V3</h2> and save the file.
4. Commit the file to the local Git repository, using either the Changes page in Visual Studio's Team Explorer
tab, or by entering the following using the local machine's command shell:
5. Using the local machine's command shell, add the staging deployment URL as a Git remote and push the
committed changes:
a. Add the remote URL for staging to the local Git repository.
b. Push the local master branch to the azure-staging remote's master branch.
az webapp deployment slot swap --name $webappname --resource-group AzureTutorial --slot staging
8. Verify that the swap occurred by refreshing the two browser windows.
Summary
In this section, the following tasks were completed:
Downloaded and built the sample app.
Created an Azure App Service Web App using the Azure Cloud Shell.
Deployed the sample app to Azure using Git.
Deployed a change to the app using Visual Studio.
Added a staging slot to the web app.
Deployed an update to the staging slot.
Swapped the staging and production slots.
In the next section, you'll learn how to build a DevOps pipeline with Azure Pipelines.
Additional reading
Web Apps overview
Build a .NET Core and SQL Database web app in Azure App Service
Configure deployment credentials for Azure App Service
Set up staging environments in Azure App Service
Continuous integration and deployment
9/10/2018 • 10 minutes to read • Edit Online
In the previous chapter, you created a local Git repository for the Simple Feed Reader app. In this chapter, you'll
publish that code to a GitHub repository and construct an Azure DevOps Services pipeline using Azure Pipelines.
The pipeline enables continuous builds and deployments of the app. Any commit to the GitHub repository triggers
a build and a deployment to the Azure Web App's staging slot.
In this section, you'll complete the following tasks:
Publish the app's code to GitHub
Disconnect local Git deployment
Create an Azure DevOps organization
Create a team project in Azure DevOps Services
Create a build definition
Create a release pipeline
Commit changes to GitHub and automatically deploy to Azure
Examine the Azure Pipelines pipeline
3. Select your account in the Owner drop-down, and enter simple-feed -reader in the Repository name
textbox.
4. Click the Create repository button.
5. Open your local machine's command shell. Navigate to the directory in which the simple-feed -reader Git
repository is stored.
6. Rename the existing origin remote to upstream. Execute the following command:
7. Add a new origin remote pointing to your copy of the repository on GitHub. Execute the following
command:
2. Click Deployment options. A new panel appears. Click Disconnect to remove the local Git source control
configuration that was added in the previous chapter. Confirm the removal operation by clicking the Yes
button.
3. Navigate to the mywebapp<unique_number> App Service. As a reminder, the portal's search box can be
used to quickly locate the App Service.
4. Click Deployment options. A new panel appears. Click Disconnect to remove the local Git source control
configuration that was added in the previous chapter. Confirm the removal operation by clicking the Yes
button.
3. Authorization is required before Azure DevOps can access your GitHub repository. Enter
<GitHub_username> GitHub connection in the Connection name textbox. For example:
4. If two-factor authentication is enabled on your GitHub account, a personal access token is required. In that
case, click the Authorize with a GitHub personal access token link. See the official GitHub personal
access token creation instructions for help. Only the repo scope of permissions is needed. Otherwise, click
the Authorize using OAuth button.
5. When prompted, sign in to your GitHub account. Then select Authorize to grant access to your Azure
DevOps organization. If successful, a new service endpoint is created.
6. Click the ellipsis button next to the Repository button. Select the <GitHub_username>/simple-feed -reader
repository from the list. Click the Select button.
7. Select the master branch from the Default branch for manual and scheduled builds drop-down. Click
the Continue button. The template selection page appears.
Create the build definition
1. From the template selection page, enter ASP.NET Core in the search box:
2. The template search results appear. Hover over the ASP.NET Core template, and click the Apply button.
3. The Tasks tab of the build definition appears. Click the Triggers tab.
4. Check the Enable continuous integration box. Under the Branch filters section, confirm that the Type
drop-down is set to Include. Set the Branch specification drop-down to master.
These settings cause a build to trigger when any change is pushed to the master branch of the GitHub
repository. Continuous integration is tested in the Commit changes to GitHub and automatically deploy to
Azure section.
5. Click the Save & queue button, and select the Save option:
3. The template search results appear. Hover over the Azure App Service Deployment with Slot template,
and click the Apply button. The Pipeline tab of the release pipeline appears.
4. Click the Add button in the Artifacts box. The Add artifact panel appears:
5. Select the Build tile from the Source type section. This type allows for the linking of the release pipeline to
the build definition.
6. Select MyFirstProject from the Project drop-down.
7. Select the build definition name, MyFirstProject-ASP.NET Core-CI, from the Source (Build definition)
drop-down.
8. Select Latest from the Default version drop-down. This option builds the artifacts produced by the latest
run of the build definition.
9. Replace the text in the Source alias textbox with Drop.
10. Click the Add button. The Artifacts section updates to display the changes.
11. Click the lightning bolt icon to enable continuous deployments:
With this option enabled, a deployment occurs each time a new build is available.
12. A Continuous deployment trigger panel appears to the right. Click the toggle button to enable the
feature. It isn't necessary to enable the Pull request trigger.
13. Click the Add drop-down in the Build branch filters section. Choose the Build Definition's default
branch option. This filter causes the release to trigger only for a build from the GitHub repository's master
branch.
14. Click the Save button. Click the OK button in the resulting Save modal dialog.
15. Click the Environment 1 box. An Environment panel appears to the right. Change the Environment 1 text
in the Environment name textbox to Production.
5. Push the change in the master branch to the origin remote of your GitHub repository:
The build is triggered, since continuous integration is enabled in the build definition's Triggers tab:
6. Navigate to the Queued tab of the Azure Pipelines > Builds page in Azure DevOps Services. The queued
build shows the branch and commit that triggered the build:
7. Once the build succeeds, a deployment to Azure occurs. Navigate to the app in the browser. Notice that the
"V4" text appears in the heading:
Examine the Azure Pipelines pipeline
Build definition
A build definition was created with the name MyFirstProject-ASP.NET Core-CI. Upon completion, the build
produces a .zip file including the assets to be published. The release pipeline deploys those assets to Azure.
The build definition's Tasks tab lists the individual steps being used. There are five build tasks.
1. Restore — Executes the dotnet restore command to restore the app's NuGet packages. The default
package feed used is nuget.org.
2. Build — Executes the dotnet build --configuration release command to compile the app's code. This
--configuration option is used to produce an optimized version of the code, which is suitable for
deployment to a production environment. Modify the BuildConfiguration variable on the build definition's
Variables tab if, for example, a debug configuration is needed.
3. Test — Executes the
dotnet test --configuration release --logger trx --results-directory <local_path_on_build_agent>
command to run the app's unit tests. Unit tests are executed within any C# project matching the
**/*Tests/*.csproj glob pattern. Test results are saved in a .trx file at the location specified by the
--results-directory option. If any tests fail, the build fails and isn't deployed.
NOTE
To verify the unit tests work, modify SimpleFeedReader.Tests\Services\NewsServiceTests.cs to purposefully break one
of the tests. For example, change Assert.True(result.Count > 0); to Assert.False(result.Count > 0); in the
Returns_News_Stories_Given_Valid_Uri method. Commit and push the change to GitHub. The build is triggered
and fails. The build pipeline status changes to failed. Revert the change, commit, and push again. The build succeeds.
On the resulting page, click the link corresponding to the unique build number:
A summary of this specific build is displayed. Click the Artifacts tab, and notice the drop folder produced by the
build is listed:
Use the Download and Explore links to inspect the published artifacts.
Release pipeline
A release pipeline was created with the name MyFirstProject-ASP.NET Core-CD:
The two major components of the release pipeline are the Artifacts and the Environments. Clicking the box in
the Artifacts section reveals the following panel:
The Source (Build definition) value represents the build definition to which this release pipeline is linked. The
.zip file produced by a successful run of the build definition is provided to the Production environment for
deployment to Azure. Click the 1 phase, 2 tasks link in the Production environment box to view the release pipeline
tasks:
The release pipeline consists of two tasks: Deploy Azure App Service to Slot and Manage Azure App Service - Slot
Swap. Clicking the first task reveals the following task configuration:
The Azure subscription, service type, web app name, resource group, and deployment slot are defined in the
deployment task. The Package or folder textbox holds the .zip file path to be extracted and deployed to the
staging slot of the mywebapp<unique_number> web app.
Clicking the slot swap task reveals the following task configuration:
The subscription, resource group, service type, web app name, and deployment slot details are provided. The Swap
with Production checkbox is checked. Consequently, the bits deployed to the staging slot are swapped into the
production environment.
Additional reading
Create your first pipeline with Azure Pipelines
Build and .NET Core project
Deploy a web app with Azure Pipelines
Monitor and debug
8/9/2018 • 4 minutes to read • Edit Online
Having deployed the app and built a DevOps pipeline, it's important to understand how to monitor and
troubleshoot the app.
In this section, you'll complete the following tasks:
Find basic monitoring and troubleshooting data in the Azure portal
Learn how Azure Monitor provides a deeper look at metrics across all Azure services
Connect the web app with Application Insights for app profiling
Turn on logging and learn where to download logs
Stream logs in real time
Learn where to set up alerts
Learn about remote debugging Azure App Service web apps.
Http 5xx: Count of server-side errors, usually exceptions in ASP.NET Core code.
Data In: Data ingress coming into your web app.
Data Out: Data egress from your web app to clients.
Requests: Count of HTTP requests.
Average Response Time: Average time for the web app to respond to HTTP requests.
Several self-service tools for troubleshooting and optimization are also found on this page.
Advanced monitoring
Azure Monitor is the centralized service for monitoring all metrics and setting alerts across Azure services. Within
Azure Monitor, administrators can granularly track performance and identify trends. Each Azure service offers its
own set of metrics to Azure Monitor.
As the app is used, data accumulates. Select Refresh to reload the blade with new data.
Application Insights provides useful server-side information with no additional configuration. To get the most
value from Application Insights, instrument your app with the Application Insights SDK. When properly
configured, the service provides end-to-end monitoring across the web server and browser, including client-side
performance. For more information, see the Application Insights documentation.
Logging
Web server and app logs are disabled by default in Azure App Service. Enable the logs with the following steps:
1. Open the Azure portal, and navigate to the mywebapp<unique_number> App Service.
2. In the menu to the left, scroll down to the Monitoring section. Select Diagnostics logs.
3. Turn on Application Logging (Filesystem ). If prompted, click the box to install the extensions to enable
app logging in the web app.
4. Set Web server logging to File System.
5. Enter the Retention Period in days. For example, 30.
6. Click Save.
ASP.NET Core and web server (App Service) logs are generated for the web app. They can be downloaded using
the FTP/FTPS information displayed. The password is the same as the deployment credentials created earlier in
this guide. The logs can be streamed directly to your local machine with PowerShell or Azure CLI. Logs can also be
viewed in Application Insights.
Log streaming
App and web server logs can be streamed in real time through the portal.
1. Open the Azure portal, and navigate to the mywebapp<unique_number> App Service.
2. In the menu to the left, scroll down to the Monitoring section and select Log stream.
Logs can also be streamed via Azure CLI or Azure PowerShell, including through the Cloud Shell.
Alerts
Azure Monitor also provides real time alerts based on metrics, administrative events, and other criteria.
Note: Currently alerting on web app metrics is only available in the Alerts (classic) service.
The Alerts (classic) service can be found in Azure Monitor or under the Monitoring section of the App Service
settings.
Live debugging
Azure App Service can be debugged remotely with Visual Studio when logs don't provide enough information.
However, remote debugging requires the app to be compiled with debug symbols. Debugging shouldn't be done
in production, except as a last resort.
Conclusion
In this section, you completed the following tasks:
Find basic monitoring and troubleshooting data in the Azure portal
Learn how Azure Monitor provides a deeper look at metrics across all Azure services
Connect the web app with Application Insights for app profiling
Turn on logging and learn where to download logs
Stream logs in real time
Learn where to set up alerts
Learn about remote debugging Azure App Service web apps.
Additional reading
Troubleshoot ASP.NET Core on Azure App Service
Common errors reference for Azure App Service and IIS with ASP.NET Core
Monitor Azure web app performance with Application Insights
Enable diagnostics logging for web apps in Azure App Service
Troubleshoot a web app in Azure App Service using Visual Studio
Create classic metric alerts in Azure Monitor for Azure services - Azure portal
Next steps
8/16/2018 • 2 minutes to read • Edit Online
In this guide, you created a DevOps pipeline for an ASP.NET Core sample app. Congratulations! We hope you
enjoyed learning to publish ASP.NET Core web apps to Azure App Service and automate the continuous
integration of changes.
Beyond web hosting and DevOps, Azure has a wide array of Platform-as-a-Service (PaaS ) services useful to
ASP.NET Core developers. This section gives a brief overview of some of the most commonly used services.
Identity
Azure Active Directory and Azure Active Directory B2C are both identity services. Azure Active Directory is
designed for enterprise scenarios and enables Azure AD B2B (business-to-business) collaboration, while Azure
Active Directory B2C is intended business-to-customer scenarios, including social network sign-in.
Mobile
Notification Hubs is a multi-platform, scalable push-notification engine to quickly send millions of messages to
apps running on various types of devices.
Web infrastructure
Azure Container Service manages your hosted Kubernetes environment, making it quick and easy to deploy and
manage containerized apps without container orchestration expertise.
Azure Search is used to create an enterprise search solution over private, heterogenous content.
Service Fabric is a distributed systems platform that makes it easy to package, deploy, and manage scalable and
reliable microservices and containers.
Client-side development in ASP.NET Core
8/30/2018 • 2 minutes to read • Edit Online
Use Gulp
Use Grunt
Use LibMan
LibMan CLI
LibMan in Visual Studio
Manage client-side packages with Bower
Build responsive sites with Bootstrap
Style apps with LESS, Sass, and Font Awesome
Bundle and minify
TypeScript
Use Browser Link
Use JavaScriptServices for SPAs
Use the SPA project templates
Angular project template
React project template
React with Redux project template
Use Gulp in ASP.NET Core
6/21/2018 • 9 minutes to read • Edit Online
Gulp
Gulp is a JavaScript-based streaming build toolkit for client-side code. It's commonly used to stream client-side
files through a series of processes when a specific event is triggered in a build environment. For instance, Gulp can
be used to automate bundling and minification or the cleansing of a development environment before a new
build.
A set of Gulp tasks is defined in gulpfile.js. The following JavaScript includes Gulp modules and specifies file paths
to be referenced within the forthcoming tasks:
var paths = {
webroot: "./wwwroot/"
};
The above code specifies which Node modules are required. The require function imports each module so that
the dependent tasks can utilize their features. Each of the imported modules is assigned to a variable. The
modules can be located either by name or path. In this example, the modules named gulp , rimraf , gulp-concat ,
gulp-cssmin , and gulp-uglify are retrieved by name. Additionally, a series of paths are created so that the
locations of CSS and JavaScript files can be reused and referenced within the tasks. The following table provides
descriptions of the modules included in gulpfile.js.
MODULE NAME DESCRIPTION
gulp The Gulp streaming build system. For more information, see
gulp.
gulp-cssmin A module that minifies CSS files. For more information, see
gulp-cssmin.
gulp-uglify A module that minifies .js files. For more information, see
gulp-uglify.
Once the requisite modules are imported, the tasks can be specified. Here there are six tasks registered,
represented by the following code:
gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});
gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
The following table provides an explanation of the tasks specified in the code above:
clean:js A task that uses the rimraf Node deletion module to remove
the minified version of the site.js file.
clean:css A task that uses the rimraf Node deletion module to remove
the minified version of the site.css file.
min:js A task that minifies and concatenates all .js files within the js
folder. The .min.js files are excluded.
min:css A task that minifies and concatenates all .css files within the
css folder. The .min.css files are excluded.
min A task that calls the min:js task, followed by the min:css
task.
var paths = {
webroot: "./wwwroot/"
};
gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});
gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
{
"devDependencies": {
"gulp": "3.9.1",
"gulp-concat": "2.6.1",
"gulp-cssmin": "0.1.7",
"gulp-uglify": "2.0.1",
"rimraf": "2.6.1"
}
}
Task Runner Explorer shows the list of Gulp tasks. (You might have to click the Refresh button that
appears to the left of the project name.)
IMPORTANT
The Task Runner Explorer context menu item appears only if gulpfile.js is in the root project directory.
4. Underneath Tasks in Task Runner Explorer, right-click clean, and select Run from the pop-up menu.
Task Runner Explorer will create a new tab named clean and execute the clean task as it's defined in
gulpfile.js.
5. Right-click the clean task, then select Bindings > Before Build.
The Before Build binding configures the clean task to run automatically before each build of the project.
The bindings you set up with Task Runner Explorer are stored in the form of a comment at the top of your
gulpfile.js and are effective only in Visual Studio. An alternative that doesn't require Visual Studio is to configure
automatic execution of gulp tasks in your .csproj file. For example, put this in your .csproj file:
Now the clean task is executed when you run the project in Visual Studio or from a command prompt using the
dotnet run command (run npm install first).
gulp.task("first", function () {
console.log('first task! <-----');
});
The output text is displayed. To see examples based on common scenarios, see Gulp Recipes.
gulp.task("series:first", function () {
console.log('first task! <-----');
});
You now have three tasks: series:first , series:second , and series . The series:second task includes a
second parameter which specifies an array of tasks to be run and completed before the series:second task
will run. As specified in the code above, only the series:first task must be completed before the
series:second task will run.
2. Save gulpfile.js.
3. In Solution Explorer, right-click gulpfile.js and select Task Runner Explorer if it isn't already open.
4. In Task Runner Explorer, right-click series and select Run.
IntelliSense
IntelliSense provides code completion, parameter descriptions, and other features to boost productivity and to
decrease errors. Gulp tasks are written in JavaScript; therefore, IntelliSense can provide assistance while
developing. As you work with JavaScript, IntelliSense lists the objects, functions, properties, and parameters that
are available based on your current context. Select a coding option from the pop-up list provided by IntelliSense
to complete the code.
task gulp.task(name[, deps], fn) { } The task function creates a task. The
name parameter defines the name of
the task. The deps parameter
contains an array of tasks to be
completed before this task runs. The
fn parameter represents a callback
function which performs the operations
of the task.
watch gulp.watch(glob [, opts], tasks) The watch function monitors files and
{ } runs tasks when a file change occurs.
The glob parameter is a string or
array that determines which files to
watch. The opts parameter provides
additional file watching options.
For additional Gulp API reference information, see Gulp Docs API.
Gulp recipes
The Gulp community provides Gulp Recipes. These recipes consist of Gulp tasks to address common scenarios.
Additional resources
Gulp documentation
Bundling and minification in ASP.NET Core
Use Grunt in ASP.NET Core
Use Grunt in ASP.NET Core
6/21/2018 • 8 minutes to read • Edit Online
By Noel Rice
Grunt is a JavaScript task runner that automates script minification, TypeScript compilation, code quality "lint"
tools, CSS pre-processors, and just about any repetitive chore that needs doing to support client development.
Grunt is fully supported in Visual Studio, though the ASP.NET project templates use Gulp by default (see Use
Gulp).
This example uses an empty ASP.NET Core project as its starting point, to show how to automate the client build
process from scratch.
The finished example cleans the target deployment directory, combines JavaScript files, checks code quality,
condenses JavaScript file content and deploys to the root of your web application. We will use the following
packages:
grunt: The Grunt task runner package.
grunt-contrib-clean: A plugin that removes files or directories.
grunt-contrib-jshint: A plugin that reviews JavaScript code quality.
grunt-contrib-concat: A plugin that joins files into a single file.
grunt-contrib-uglify: A plugin that minifies JavaScript to reduce size.
grunt-contrib-watch: A plugin that watches file activity.
6. Right-click the TypeScript directory and select Add > New Item from the context menu. Select the
JavaScript file item and name the file Tastes.ts (note the *.ts extension). Copy the line of TypeScript code
below into the file (when you save, a new Tastes.js file will appear with the JavaScript source).
7. Add a second file to the TypeScript directory and name it Food.ts . Copy the code below into the file.
class Food {
constructor(name: string, calories: number) {
this._name = name;
this._calories = calories;
}
Configuring NPM
Next, configure NPM to download grunt and grunt-tasks.
1. In the Solution Explorer, right-click the project and select Add > New Item from the context menu. Select
the NPM configuration file item, leave the default name, package.json, and click the Add button.
2. In the package.json file, inside the devDependencies object braces, enter "grunt". Select grunt from the
Intellisense list and press the Enter key. Visual Studio will quote the grunt package name, and add a colon.
To the right of the colon, select the latest stable version of the package from the top of the Intellisense list
(press Ctrl-Space if Intellisense doesn't appear).
NOTE
NPM uses semantic versioning to organize dependencies. Semantic versioning, also known as SemVer, identifies
packages with the numbering scheme ... Intellisense simplifies semantic versioning by showing only a few common
choices. The top item in the Intellisense list (0.4.5 in the example above) is considered the latest stable version of the
package. The caret (^) symbol matches the most recent major version and the tilde (~) matches the most recent
minor version. See the NPM semver version parser reference as a guide to the full expressivity that SemVer provides.
3. Add more dependencies to load grunt-contrib-* packages for clean, jshint, concat, uglify, and watch as
shown in the example below. The versions don't need to match the example.
"devDependencies": {
"grunt": "0.4.5",
"grunt-contrib-clean": "0.6.0",
"grunt-contrib-jshint": "0.11.0",
"grunt-contrib-concat": "0.5.1",
"grunt-contrib-uglify": "0.8.0",
"grunt-contrib-watch": "0.6.1"
}
NOTE
If you need to, you can manually restore dependencies in Solution Explorer by right-clicking on Dependencies\NPM and
selecting the Restore Packages menu option.
Configuring Grunt
Grunt is configured using a manifest named Gruntfile.js that defines, loads and registers tasks that can be run
manually or configured to run automatically based on events in Visual Studio.
1. Right-click the project and select Add > New Item. Select the Grunt Configuration file option, leave the
default name, Gruntfile.js, and click the Add button.
The initial code includes a module definition and the grunt.initConfig() method. The initConfig() is
used to set options for each package, and the remainder of the module will load and register tasks.
2. Inside the initConfig() method, add options for the clean task as shown in the example Gruntfile.js
below. The clean task accepts an array of directory strings. This task removes files from wwwroot/lib and
removes the entire /temp directory.
3. Below the initConfig() method, add a call to grunt.loadNpmTasks() . This will make the task runnable from
Visual Studio.
grunt.loadNpmTasks("grunt-contrib-clean");
4. Save Gruntfile.js. The file should look something like the screenshot below.
5. Right-click Gruntfile.js and select Task Runner Explorer from the context menu. The Task Runner Explorer
window will open.
6. Verify that clean shows under Tasks in the Task Runner Explorer.
7. Right-click the clean task and select Run from the context menu. A command window displays progress of
the task.
NOTE
There are no files or directories to clean yet. If you like, you can manually create them in the Solution Explorer and
then run the clean task as a test.
8. In the initConfig() method, add an entry for concat using the code below.
The src property array lists files to combine, in the order that they should be combined. The dest
property assigns the path to the combined file that's produced.
concat: {
all: {
src: ['TypeScript/Tastes.js', 'TypeScript/Food.js'],
dest: 'temp/combined.js'
}
},
NOTE
The all property in the code above is the name of a target. Targets are used in some Grunt tasks to allow
multiple build environments. You can view the built-in targets using Intellisense or assign your own.
jshint: {
files: ['temp/*.js'],
options: {
'-W069': false,
}
},
NOTE
The option "-W069" is an error produced by jshint when JavaScript uses bracket syntax to assign a property instead
of dot notation, i.e. Tastes["Sweet"] instead of Tastes.Sweet . The option turns off the warning to allow the rest
of the process to continue.
uglify: {
all: {
src: ['temp/combined.js'],
dest: 'wwwroot/lib/combined.min.js'
}
},
11. Under the call grunt.loadNpmTasks() that loads grunt-contrib-clean, include the same call for jshint, concat
and uglify using the code below.
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
12. Save Gruntfile.js. The file should look something like the example below.
13. Notice that the Task Runner Explorer Tasks list includes clean , concat , jshint and uglify tasks. Run
each task in order and observe the results in Solution Explorer. Each task should run without errors.
The concat task creates a new combined.js file and places it into the temp directory. The jshint task simply
runs and doesn't produce output. The uglify task creates a new combined.min.js file and places it into
wwwroot/lib. On completion, the solution should look something like the screenshot below:
NOTE
For more information on the options for each package, visit https://www.npmjs.com/ and lookup the package name
in the search box on the main page. For example, you can look up the grunt-contrib-clean package to get a
documentation link that explains all of its parameters.
The new task shows up in Task Runner Explorer under Alias Tasks. You can right-click and run it just as you would
other tasks. The all task will run clean , concat , jshint and uglify , in order.
Watching for changes
A watch task keeps an eye on files and directories. The watch triggers tasks automatically if it detects changes.
Add the code below to initConfig to watch for changes to *.js files in the TypeScript directory. If a JavaScript file is
changed, watch will run the all task.
watch: {
files: ["TypeScript/*.js"],
tasks: ["all"]
}
Add a call to loadNpmTasks() to show the watch task in Task Runner Explorer.
grunt.loadNpmTasks('grunt-contrib-watch');
Right-click the watch task in Task Runner Explorer and select Run from the context menu. The command window
that shows the watch task running will display a "Waiting…" message. Open one of the TypeScript files, add a
space, and then save the file. This will trigger the watch task and trigger the other tasks to run in order. The
screenshot below shows a sample run.
Unload and reload the project. When the project loads again, the watch task will start running automatically.
Summary
Grunt is a powerful task runner that can be used to automate most client-build tasks. Grunt leverages NPM to
deliver its packages, and features tooling integration with Visual Studio. Visual Studio's Task Runner Explorer
detects changes to configuration files and provides a convenient interface to run tasks, view running tasks, and
bind tasks to Visual Studio events.
Additional resources
Use Gulp
Client-side library acquisition in ASP.NET Core with
LibMan
8/30/2018 • 2 minutes to read • Edit Online
By Scott Addie
Library Manager (LibMan) is a lightweight, client-side library acquisition tool. LibMan downloads popular
libraries and frameworks from the file system or from a content delivery network (CDN ) . The supported CDNs
include CDNJS and unpkg. The selected library files are fetched and placed in the appropriate location within the
ASP.NET Core project.
Additional resources
Use LibMan with ASP.NET Core in Visual Studio
Use the LibMan command-line interface (CLI) with ASP.NET Core
LibMan GitHub repository
Use the LibMan command-line interface (CLI) with
ASP.NET Core
9/27/2018 • 8 minutes to read • Edit Online
By Scott Addie
The LibMan CLI is a cross-platform tool that's supported everywhere .NET Core is supported.
Prerequisites
.NET Core 2.1 SDK or later
Installation
To install the LibMan CLI:
A .NET Core Global Tool is installed from the Microsoft.Web.LibraryManager.Cli NuGet package.
To install the LibMan CLI from a specific NuGet package source:
In the preceding example, a .NET Core Global Tool is installed from the local Windows machine's
C:\Temp\Microsoft.Web.LibraryManager.Cli.1.0.94 -g606058a278.nupkg file.
Usage
After successful installation of the CLI, the following command can be used:
libman
libman --version
libman --help
Options:
--help|-h Show help information
--version Show version information
Commands:
cache List or clean libman cache contents
clean Deletes all library files defined in libman.json from the project
init Create a new libman.json
install Add a library definition to the libman.json file, and download the
library to the specified location
restore Downloads all files from provider and saves them to specified
destination
uninstall Deletes all files for the specified library from their specified
destination, then removes the specified library definition from
libman.json
update Updates the specified library
Options
The following options are available for the libman init command:
-d|--default-destination <PATH>
A path relative to the current folder. Library files are installed in this location if no destination property is
defined for a library in libman.json. The <PATH> value is written to the defaultDestination property of
libman.json.
-p|--default-provider <PROVIDER>
The provider to use if no provider is defined for a given library. The <PROVIDER> value is written to the
defaultProvider property of libman.json. Replace <PROVIDER> with one of the following values:
cdnjs
filesystem
unpkg
-h|--help
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
To create a libman.json file in an ASP.NET Core project:
Navigate to the project root.
Run the following command:
libman init
Type the name of the default provider, or press Enter to use the default CDNJS provider. Valid values
include:
cdnjs
filesystem
unpkg
A libman.json file is added to the project root with the following content:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}
Arguments
LIBRARY
The name of the library to install. This name may include version number notation (for example, @1.2.0 ).
Options
The following options are available for the libman install command:
-d|--destination <PATH>
The location to install the library. If not specified, the default location is used. If no defaultDestination
property is specified in libman.json, this option is required.
--files <FILE>
Specify the name of the file to install from the library. If not specified, all files from the library are installed.
Provide one --files option per file to be installed. Relative paths are supported too. For example:
--files dist/browser/signalr.js .
-p|--provider <PROVIDER>
The name of the provider to use for the library acquisition. Replace <PROVIDER> with one of the following
values:
cdnjs
filesystem
unpkg
If not specified, the defaultProvider property in libman.json is used. If no defaultProvider property is
specified in libman.json, this option is required.
-h|--help
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
Consider the following libman.json file:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": []
}
To install the jQuery version 3.2.1 jquery.min.js file to the wwwroot/scripts/jquery folder using the CDNJS
provider:
To install the calendar.js and calendar.css files from C:\temp\contosoCalendar\ using the file system provider:
After accepting the default destination, the libman.json file resembles the following:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "[email protected]",
"destination": "wwwroot/scripts/jquery",
"files": [
"jquery.min.js"
]
},
{
"library": "C:\\temp\\contosoCalendar\\",
"provider": "filesystem",
"destination": "wwwroot/lib/contosoCalendar",
"files": [
"calendar.js",
"calendar.css"
]
}
]
}
Restore library files
The libman restore command installs library files defined in libman.json. The following rules apply:
If no libman.json file exists in the project root, an error is returned.
If a library specifies a provider, the defaultProvider property in libman.json is ignored.
If a library specifies a destination, the defaultDestination property in libman.json is ignored.
Synopsis
Options
The following options are available for the libman restore command:
-h|--help
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
To restore the library files defined in libman.json:
libman restore
Options
The following options are available for the libman clean command:
-h|--help
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
To delete library files installed via LibMan:
libman clean
Arguments
LIBRARY
The name of the library to uninstall. This name may include version number notation (for example, @1.2.0 ).
Options
The following options are available for the libman uninstall command:
-h|--help
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
Consider the following libman.json file:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "[email protected]",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "unpkg",
"library": "[email protected]",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "filesystem",
"library": "C:\\temp\\lodash\\",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}
Arguments
LIBRARY
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
To update jQuery to the latest version:
Arguments
PROVIDER
Only used with the clean command. Specifies the provider cache to clean. Valid values include:
cdnjs
filesystem
unpkg
Options
The following options are available for the libman cache command:
--files
Set the verbosity of the output. Replace <LEVEL> with one of the following values:
quiet
normal
detailed
Examples
To view the names of cached libraries per provider, use one of the following commands:
Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
font-awesome
jquery
knockout
lodash.js
react
Notice the preceding output shows that jQuery versions 3.2.1 and 3.3.1 are cached under the CDNJS
provider.
To empty the library cache for the CDNJS provider:
After emptying the CDNJS provider cache, the libman cache list command displays the following:
Cache contents:
---------------
unpkg:
knockout
react
vue
cdnjs:
(empty)
Cache contents:
---------------
unpkg:
(empty)
cdnjs:
(empty)
Additional resources
Install a Global Tool
Use LibMan with ASP.NET Core in Visual Studio
LibMan GitHub repository
Use LibMan with ASP.NET Core in Visual Studio
8/30/2018 • 7 minutes to read • Edit Online
By Scott Addie
Visual Studio has built-in support for LibMan in ASP.NET Core projects, including:
Support for configuring and running LibMan restore operations on build.
Menu items for triggering LibMan restore and clean operations.
Search dialog for finding libraries and adding the files to a project.
Editing support for libman.json—the LibMan manifest file.
View or download sample code (how to download)
Prerequisites
Visual Studio 2017 version 15.8 or later with the ASP.NET and web development workload
Select the library provider from the Provider drop down. CDNJS is the default provider.
Type the library name to fetch in the Library text box. IntelliSense provides a list of libraries beginning with
the provided text.
Select the library from the IntelliSense list. Notice the library name is suffixed with the @ symbol and the
latest stable version known to the selected provider.
Decide which files to include:
Select the Include all library files radio button to include all of the library's files.
Select the Choose specific files radio button to include a subset of the library's files. When the radio
button is selected, the file selector tree is enabled. Check the boxes to the left of the file names to
download.
Specify the project folder for storing the files in the Target Location text box. As a recommendation, store
each library in a separate folder.
The suggested Target Location folder is based on the location from which the dialog launched:
If launched from the project root:
wwwroot/lib is used if wwwroot exists.
lib is used if wwwroot doesn't exist.
If launched from a project folder, the corresponding folder name is used.
The folder suggestion is suffixed with the library name. The following table illustrates folder suggestions
when installing jQuery in a Razor Pages project.
Click the Install button to download the files, per the configuration in libman.json.
Review the Library Manager feed of the Output window for installation details. For example:
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "[email protected]",
"files": [
"jquery.min.js",
"jquery.js",
"jquery.min.map"
],
"destination": "wwwroot/lib/jquery/"
},
{
"provider": "unpkg",
"library": "[email protected]",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "filesystem",
"library": "C:\\temp\\lodash\\",
"files": [
"lodash.js",
"lodash.min.js"
],
"destination": "wwwroot/lib/lodash/"
}
]
}
NOTE
LibMan only supports one version of each library from each provider. The libman.json file fails schema validation if it
contains two libraries with the same library name for a given provider.
Build the project to confirm LibMan file restoration occurs. The Microsoft.Web.LibraryManager.Build
package injects an MSBuild target that runs LibMan during the project's build operation.
Review the Build feed of the Output window for a LibMan activity log:
1>------ Build started: Project: LibManSample, Configuration: Debug Any CPU ------
1>
1>Restore operation started...
1>Restoring library [email protected]...
1>Restoring library [email protected]...
1>
1>2 libraries restored in 10.66 seconds
1>LibManSample -> C:\LibManSample\bin\Debug\netcoreapp2.1\LibManSample.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
When the restore-on-build behavior is enabled, the libman.json context menu displays a Disable Restore Client-
Side Libraries on Build option. Selecting this option removes the Microsoft.Web.LibraryManager.Build package
reference from the project file. Consequently, the client-side libraries are no longer restored on each build.
Regardless of the restore-on-build setting, you can manually restore at any time from the libman.json context
menu. For more information, see Restore files manually.
Restore files manually
To manually restore library files:
For all projects in the solution:
Right-click the solution name in Solution Explorer.
Select the Restore Client-Side Libraries option.
For a specific project:
Right-click the libman.json file in Solution Explorer.
Select the Restore Client-Side Libraries option.
While the restore operation is running:
The Task Status Center (TSC ) icon on the Visual Studio status bar will be animated and will read Restore
operation started. Clicking the icon opens a tooltip listing the known background tasks.
Messages will be sent to the status bar and the Library Manager feed of the Output window. For
example:
Restore operation started...
Restoring libraries for project LibManSample
Restoring library [email protected]... (LibManSample)
wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
Restore operation completed
1 libraries restored in 2.32 seconds
The clean operation only deletes files from the project. Library files stay in the cache for faster retrieval on future
restore operations. To manage library files stored in the local machine's cache, use the LibMan CLI.
Alternatively, you can manually edit and save the LibMan manifest (libman.json). The restore operation runs when
the file is saved. Library files that are no longer defined in libman.json are removed from the project.
Update library version
To check for an updated library version:
Open libman.json.
Position the caret inside the corresponding libraries object literal.
Click the light bulb icon that appears in the left margin. Hover over Check for updates.
LibMan checks for a library version newer than the version installed. The following outcomes can occur:
A No updates found message is displayed if the latest version is already installed.
The latest stable version is displayed if not already installed.
If a pre-release newer than the installed version is available, the pre-release is displayed.
To downgrade to an older library version, manually edit the libman.json file. When the file is saved, the LibMan
restore operation:
Removes redundant files from the previous version.
Adds new and updated files from the new version.
Additional resources
Use the LibMan command-line interface (CLI) with ASP.NET Core
LibMan GitHub repository
Manage client-side packages with Bower in ASP.NET
Core
8/22/2018 • 5 minutes to read • Edit Online
IMPORTANT
While Bower is maintained, its maintainers recommend using a different solution. Library Manager (LibMan for short) is
Visual Studio's new client-side library acquisition tool (Visual Studio 15.8 or later). For more information, see Client-side
library acquisition in ASP.NET Core with LibMan. Bower is supported in Visual Studio through version 15.5.
Yarn with Webpack is one popular alternative for which migration instructions are available.
Bower calls itself "A package manager for the web". Within the .NET ecosystem, it fills the void left by NuGet's
inability to deliver static content files. For ASP.NET Core projects, these static files are inherent to client-side
libraries like jQuery and Bootstrap. For .NET libraries, you still use NuGet package manager.
New projects created with the ASP.NET Core project templates set up the client-side build process. jQuery and
Bootstrap are installed, and Bower is supported.
Client-side packages are listed in the bower.json file. The ASP.NET Core project templates configures bower.json
with jQuery, jQuery validation, and Bootstrap.
In this tutorial, we'll add support for Font Awesome. Bower packages can be installed with the Manage Bower
Packages UI or manually in the bower.json file.
Installation via Manage Bower Packages UI
Create a new ASP.NET Core Web app with the ASP.NET Core Web Application (.NET Core) template.
Select Web Application and No Authentication.
Right-click the project in Solution Explorer and select Manage Bower Packages (alternatively from the
main menu, Project > Manage Bower Packages).
In the Bower: <project name> window, click the "Browse" tab, and then filter the packages list by entering
font-awesome in the search box:
Confirm that the "Save changes to bower.json" checkbox is checked. Select a version from the drop-down
list and click the Install button. The Output window shows the installation details.
Manual installation in bower.json
Open the bower.json file and add "font-awesome" to the dependencies. IntelliSense shows the available packages.
When a package is selected, the available versions are displayed. The images below are older and won't match
what you see.
Bower uses semantic versioning to organize dependencies. Semantic versioning, also known as SemVer, identifies
packages with the numbering scheme <major>.<minor>.<patch>. IntelliSense simplifies semantic versioning by
showing only a few common choices. The top item in the IntelliSense list (4.6.3 in the example above) is
considered the latest stable version of the package. The caret (^) symbol matches the most recent major version
and the tilde (~) matches the most recent minor version.
Save the bower.json file. Visual Studio watches the bower.json file for changes. Upon saving, the bower install
command is executed. See the Output window's Bower/npm view for the exact command executed.
Open the .bowerrc file under bower.json. The directory property is set to wwwroot/lib which indicates the
location Bower will install the package assets.
{
"directory": "wwwroot/lib"
}
You can use the search box in Solution Explorer to find and display the font-awesome package.
Open the Views\Shared_Layout.cshtml file and add the font-awesome CSS file to the environment Tag Helper for
Development . From Solution Explorer, drag and drop font-awesome.css inside the
<environment names="Development"> element.
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
<link href="~/lib/font-awesome/css/font-awesome.css" rel="stylesheet" />
</environment>
In a production app you would add font-awesome.min.css to the environment tag helper for Staging,Production .
Replace the contents of the Views\Home\About.cshtml Razor file with the following markup:
@{
ViewData["Title"] = "About";
}
<div class="list-group">
<a class="list-group-item" href="#"><i class="fa fa-home fa-fw" aria-hidden="true"></i> Home</a>
<a class="list-group-item" href="#"><i class="fa fa-book fa-fw" aria-hidden="true"></i> Library</a>
<a class="list-group-item" href="#"><i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
Applications</a>
<a class="list-group-item" href="#"><i class="fa fa-cog fa-fw" aria-hidden="true"></i> Settings</a>
</div>
Run the app and navigate to the About view to verify the font-awesome package works.
{
"name": "asp.net",
"private": true,
"dependencies": {
"jquery": "3.1.1",
"bootstrap": "3.3.7"
}
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
Reference packages
In this section, you will create an HTML page to verify it can access the deployed packages.
Add a new HTML page named Index.html to the wwwroot folder. Note: You must add the HTML file to the
wwwroot folder. By default, static content cannot be served outside wwwroot. See Static files for more
information.
Replace the contents of Index.html with the following markup:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Bower Example</title>
<link href="lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="jumbotron">
<h1>Using the jumbotron style</h1>
<p>
<a class="btn btn-primary btn-lg" role="button">Stateful button</a>
</p>
</div>
<script src="lib/jquery/dist/jquery.js"></script>
<script src="lib/bootstrap/dist/js/bootstrap.js"></script>
<script>
$(".btn").click(function () {
$(this).text('loading')
.delay(1000)
.queue(function () {
$(this).text('reset');
$(this).dequeue();
});
});
</script>
</body>
</html>
Run the app and navigate to http://localhost:<port>/Index.html . Alternatively, with Index.html opened,
press Ctrl+Shift+W . Verify that the jumbotron styling is applied, the jQuery code responds when the button
is clicked, and that the Bootstrap button changes state.
Build beautiful, responsive sites with Bootstrap and
ASP.NET Core
8/16/2018 • 11 minutes to read • Edit Online
By Steve Smith
Bootstrap is currently the most popular web framework for developing responsive web applications. It offers a
number of features and benefits that can improve your users' experience with your web site, whether you're a
novice at front-end design and development or an expert. Bootstrap is deployed as a set of CSS and JavaScript
files, and is designed to help your website or application scale efficiently from phones to tablets to desktops.
Get started
There are several ways to get started with Bootstrap. If you're starting a new web application in Visual Studio, you
can choose the default starter template for ASP.NET Core, in which case Bootstrap will come pre-installed:
Adding Bootstrap to an ASP.NET Core project is simply a matter of adding it to bower.json as a dependency:
{
"name": "asp.net",
"private": true,
"dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.0",
"jquery-validation": "1.14.0",
"jquery-validation-unobtrusive": "3.2.6"
}
}
npm
NuGet
Install-Package bootstrap
NOTE
The recommended way to install client-side dependencies like Bootstrap in ASP.NET Core is via Bower (using bower.json, as
shown above). The use of npm/NuGet are shown to demonstrate how easily Bootstrap can be added to other kinds of web
applications, including earlier versions of ASP.NET.
If you're referencing your own local versions of Bootstrap, you'll need to reference them in any pages that will use
it. In production you should reference bootstrap using a CDN. In the default ASP.NET Core site template, the
_Layout.cshtml file does so like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-
brand">WebApplication1</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
@await Html.PartialAsync("_LoginPartial")
@await Html.PartialAsync("_LoginPartial")
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2016 - WebApplication1</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
NOTE
If you're going to be using any of Bootstrap's jQuery plugins, you will also need to reference jQuery.
It also includes the application name, which appears in the top left. The main navigation menu is rendered by the
<ul> element within the second div, and includes links to Home, About, and Contact. Below the navigation, the
main body of each page is rendered in another <div> , marked with the "container" and "body-content" classes. In
the simple default _Layout file shown here, the contents of the page are rendered by the specific View associated
with the page, and then a simple <footer> is added to the end of the <div> element. You can see how the built-in
About page appears using this template:
The collapsed navbar, with "hamburger" button in the top right, appears when the window drops below a certain
width:
Clicking the icon reveals the menu items in a vertical drawer that slides down from the top of the page:
Typography and links
Bootstrap sets up the site's basic typography, colors, and link formatting in its CSS file. This CSS file includes
default styles for tables, buttons, form elements, images, and more (learn more). One particularly useful feature is
the grid layout system, covered next.
Grids
One of the most popular features of Bootstrap is its grid layout system. Modern web applications should avoid
using the <table> tag for layout, instead restricting the use of this element to actual tabular data. Instead,
columns and rows can be laid out using a series of <div> elements and the appropriate CSS classes. There are
several advantages to this approach, including the ability to adjust the layout of grids to display vertically on
narrow screens, such as on phones.
Bootstrap's grid layout system is based on twelve columns. This number was chosen because it can be divided
evenly into 1, 2, 3, or 4 columns, and column widths can vary to within 1/12th of the vertical width of the screen.
To start using the grid layout system, you should begin with a container <div> and then add a row <div> , as
shown here:
<div class="container">
<div class="row">
...
</div>
</div>
Next, add additional <div> elements for each column, and specify the number of columns that <div> should
occupy (out of 12) as part of a CSS class starting with "col-md-". For instance, if you want to simply have two
columns of equal size, you would use a class of "col-md-6" for each one. In this case "md" is short for "medium"
and refers to standard-sized desktop computer display sizes. There are four different options you can choose from,
and each will be used for higher widths unless overridden (so if you want the layout to be fixed regardless of
screen width, you can just specify xs classes).
CSS CLASS PREFIX DEVICE TIER WIDTH
When specifying two columns both with "col-md-6" the resulting layout will be two columns at desktop
resolutions, but these two columns will stack vertically when rendered on smaller devices (or a narrower browser
window on a desktop), allowing users to easily view content without the need to scroll horizontally.
Bootstrap will always default to a single-column layout, so you only need to specify columns when you want more
than one column. The only time you would want to explicitly specify that a <div> take up all 12 columns would be
to override the behavior of a larger device tier. When specifying multiple device tier classes, you may need to reset
the column rendering at certain points. Adding a clearfix div that's only visible within a certain viewport can
achieve this, as shown here:
In the above example, One and Two share a row in the "md" layout, while Two and Three share a row in the "xs"
layout. Without the clearfix <div> , Two and Three are not shown correctly in the "xs" view (note that only One,
Four, and Five are shown):
In this example, only a single row <div> was used, and Bootstrap still mostly did the right thing with regard to the
layout and stacking of the columns. Typically, you should specify a row <div> for each horizontal row your layout
requires, and of course you can nest Bootstrap grids within one another. When you do, each nested grid will
occupy 100% of the width of the element in which it's placed, which can then be subdivided using column classes.
Jumbotron
If you've used the default ASP.NET Core MVC templates in Visual Studio 2012 or 2013, you've probably seen the
Jumbotron in action. It refers to a large full-width section of a page that can be used to display a large background
image, a call to action, a rotator, or similar elements. To add a jumbotron to a page, simply add a <div> and give it
a class of "jumbotron", then place a container <div> inside and add your content. We can easily adjust the
standard About page to use a jumbotron for the main headings it displays:
Buttons
The default button classes and their colors are shown in the figure below.
Badges
Badges refer to small, usually numeric callouts next to a navigation item. They can indicate a number of messages
or notifications waiting, or the presence of updates. Specifying such badges is as simple as adding a <span>
containing the text, with a class of "badge":
Alerts
You may need to display some kind of notification, alert, or error message to your application's users. That's where
the standard alert classes are useful. There are four different severity levels with associated color schemes:
Navbars and menus
Our layout already includes a standard navbar, but the Bootstrap theme supports additional styling options. We
can also easily opt to display the navbar vertically rather than horizontally if that's preferred, as well as adding
sub-navigation items in flyout menus. Simple navigation menus, like tab strips, are built on top of <ul> elements.
These can be created very simply by just providing them with the CSS classes "nav" and "nav-tabs":
Navbars are built similarly, but are a bit more complex. They start with a <nav> or <div> with a class of "navbar",
within which a container div holds the rest of the elements. Our page includes a navbar in its header already – the
one shown below simply expands on this, adding support for a dropdown menu:
Additional elements
The default theme can also be used to present HTML tables in a nicely formatted style, including support for
striped views. There are labels with styles that are similar to those of the buttons. You can create custom
Dropdown menus that support additional styling options beyond the standard HTML <select> element, along
with Navbars like the one our default starter site is already using. If you need a progress bar, there are several
styles to choose from, as well as List Groups and panels that include a title and content. Explore additional options
within the standard Bootstrap Theme here:
http://getbootstrap.com/examples/theme/
More themes
You can extend the standard Bootstrap theme by overriding some or all of its CSS, adjusting the colors and styles
to suit your own application's needs. If you'd like to start from a ready-made theme, there are several theme
galleries available online that specialize in Bootstrap themes, such as WrapBootstrap.com (which has a variety of
commercial themes) and Bootswatch.com (which offers free themes). Some of the paid templates available
provide a great deal of functionality on top of the basic Bootstrap theme, such as rich support for administrative
menus, and dashboards with rich charts and gauges. An example of a popular paid template is Inspinia, which is
shown in the following screenshot:
If you want to change your Bootstrap theme, put the bootstrap.css file for the theme you want in the
wwwroot/css folder and change the references in _Layout.cshtml to point it. Change the links for all
environments:
<environment names="Development">
<link rel="stylesheet" href="~/css/bootstrap.css" />
<environment names="Staging,Production">
<link rel="stylesheet" href="~/css/bootstrap.min.css" />
If you want to build your own dashboard, you can start from the free example available here:
http://getbootstrap.com/examples/dashboard/.
Components
In addition to those elements already discussed, Bootstrap includes support for a variety of built-in UI
components.
Glyphicons
Bootstrap includes icon sets from Glyphicons (http://glyphicons.com), with over 200 icons freely available for use
within your Bootstrap-enabled web application. Here's just a small sample:
Input groups
Input groups allow bundling of additional text or buttons with an input element, providing the user with a more
intuitive experience:
Breadcrumbs
Breadcrumbs are a common UI component used to show a user their recent history or depth within a site's
navigation hierarchy. Add them easily by applying the "breadcrumb" class to any <ol> list element. Include built-
in support for pagination by using the "pagination" class on a <ul> element within a <nav> . Add responsive
embedded slideshows and video by using <iframe> , <embed> , <video> , or <object> elements, which Bootstrap
will style automatically. Specify a particular aspect ratio by using specific classes like "embed-responsive-16by9".
JavaScript support
Bootstrap's JavaScript library includes API support for the included components, allowing you to control their
behavior programmatically within your application. In addition, bootstrap.js includes over a dozen custom jQuery
plugins, providing additional features like transitions, modal dialogs, scroll detection (updating styles based on
where the user has scrolled in the document), collapse behavior, carousels, and affixing menus to the window so
they don't scroll off the screen. There's not sufficient room to cover all of the JavaScript add-ons built into
Bootstrap – to learn more please visit http://getbootstrap.com/javascript/.
Summary
Bootstrap provides a web framework that can be used to quickly and productively lay out and style a wide variety
of websites and applications. Its basic typography and styles provide a pleasant look and feel that can easily be
manipulated through custom theme support, which can be hand-crafted or purchased commercially. It supports a
host of web components that in the past would've required expensive third-party controls to accomplish, while
supporting modern and open web standards.
Less, Sass, and Font Awesome in ASP.NET Core
6/21/2018 • 12 minutes to read • Edit Online
By Steve Smith
Users of web applications have increasingly high expectations when it comes to style and overall experience.
Modern web applications frequently leverage rich tools and frameworks for defining and managing their look and
feel in a consistent manner. Frameworks like Bootstrap can go a long way toward defining a common set of styles
and layout options for web sites. However, most non-trivial sites also benefit from being able to effectively define
and maintain styles and cascading style sheet (CSS ) files, as well as having easy access to non-image icons that
help make the site's interface more intuitive. That's where languages and tools that support Less and Sass, and
libraries like Font Awesome, come in.
.header {
color: black;
font-weight: bold;
font-size: 18px;
font-family: Helvetica, Arial, sans-serif;
}
.small-header {
color: black;
font-weight: bold;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
}
Using Less, this can be rewritten to eliminate all of the duplication, using a mixin (so named because it allows you
to "mix in" properties from one class or rule-set into another):
.header {
color: black;
font-weight: bold;
font-size: 18px;
font-family: Helvetica, Arial, sans-serif;
}
.small-header {
.header;
font-size: 14px;
}
Less
The Less CSS preprocessor runs using Node.js. To install Less, use Node Package Manager (npm) from a
command prompt (-g means "global"):
If you're using Visual Studio, you can get started with Less by adding one or more Less files to your project, and
then configuring Gulp (or Grunt) to process them at compile-time. Add a Styles folder to your project, and then
add a new Less file named main.less to this folder.
Once added, your folder structure should look something like this:
Now you can add some basic styling to the file, which will be compiled into CSS and deployed to the wwwroot
folder by Gulp.
Modify main.less to include the following content, which creates a simple color palette from a single base color.
@base: #663333;
@background: spin(@base, 180);
@lighter: lighten(spin(@base, 5), 10%);
@lighter2: lighten(spin(@base, 10), 20%);
@darker: darken(spin(@base, -5), 10%);
@darker2: darken(spin(@base, -10), 20%);
body {
background-color:@background;
}
.baseColor {color:@base}
.bgLight {color:@lighter}
.bgLight2 {color:@lighter2}
.bgDark {color:@darker}
.bgDark2 {color:@darker2}
@base and the other @-prefixed items are variables. Each of them represents a color. Except for @base , they're set
using color functions: lighten, darken, and spin. Lighten and darken do pretty much what you would expect; spin
adjusts the hue of a color by a number of degrees (around the color wheel). The Less processor is smart enough to
ignore variables that aren't used, so to demonstrate how these variables work, we need to use them somewhere.
The classes .baseColor , etc. will demonstrate the calculated values of each of the variables in the CSS file that's
produced.
Get started
Create an npm Configuration File (package.json) in your project folder and edit it to reference gulp and
gulp-less :
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"gulp": "3.9.1",
"gulp-less": "3.3.0"
}
}
Install the dependencies either at a command prompt in your project folder, or in Visual Studio Solution Explorer
(Dependencies > npm > Restore packages).
npm install
In the project folder, create a Gulp Configuration File (gulpfile.js) to define the automated process. Add a variable
at the top of the file to represent Less, and a task to run Less:
gulp.task("less", function () {
return gulp.src('Styles/main.less')
.pipe(less())
.pipe(gulp.dest('wwwroot/css'));
});
Open the Task Runner Explorer (View > Other Windows > Task Runner Explorer). Among the tasks, you
should see a new task named less . You might have to refresh the window.
Run the less task, and you see output similar to what is shown here:
body {
background-color: #336666;
}
.baseColor {
color: #663333;
}
.bgLight {
color: #884a44;
}
.bgLight2 {
color: #aa6355;
}
.bgDark {
color: #442225;
}
.bgDark2 {
color: #221114;
}
Add a simple HTML page to the wwwroot folder, and reference main.css to see the color palette in action.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link href="css/main.css" rel="stylesheet" />
<title></title>
</head>
<body>
<div>
<div class="baseColor">BaseColor</div>
<div class="bgLight">Light</div>
<div class="bgLight2">Light2</div>
<div class="bgDark">Dark</div>
<div class="bgDark2">Dark2</div>
</div>
</body>
</html>
You can see that the 180 degree spin on @base used to produce @background resulted in the color wheel opposing
color of @base :
Less also provides support for nested rules, as well as nested media queries. For example, defining nested
hierarchies like menus can result in verbose CSS rules like these:
nav {
height: 40px;
width: 100%;
}
nav li {
height: 38px;
width: 100px;
}
nav li a:link {
color: #000;
text-decoration: none;
}
nav li a:visited {
text-decoration: none;
color: #CC3333;
}
nav li a:hover {
text-decoration: underline;
font-weight: bold;
}
nav li a:active {
text-decoration: underline;
}
Ideally all of the related style rules will be placed together within the CSS file, but in practice there's nothing
enforcing this rule except convention and perhaps block comments.
Defining these same rules using Less looks like this:
nav {
height: 40px;
width: 100%;
li {
height: 38px;
width: 100px;
a {
color: #000;
&:link { text-decoration:none}
&:visited { color: #CC3333; text-decoration:none}
&:hover { text-decoration:underline; font-weight:bold}
&:active {text-decoration:underline}
}
}
}
Note that in this case, all of the subordinate elements of nav are contained within its scope. There's no longer any
repetition of parent elements ( nav , li , a ), and the total line count has dropped as well (though some of that's a
result of putting values on the same lines in the second example). It can be very helpful, organizationally, to see all
of the rules for a given UI element within an explicitly bounded scope, in this case set off from the rest of the file by
curly braces.
The & syntax is a Less selector feature, with & representing the current selector parent. So, within the a {...} block,
& represents an a tag, and thus &:link is equivalent to a:link .
Media queries, extremely useful in creating responsive designs, can also contribute heavily to repetition and
complexity in CSS. Less allows media queries to be nested within classes, so that the entire class definition doesn't
need to be repeated within different top-level @media elements. For example, here is CSS for a responsive menu:
.navigation {
margin-top: 30%;
width: 100%;
}
@media screen and (min-width: 40em) {
.navigation {
margin: 0;
}
}
@media screen and (min-width: 62em) {
.navigation {
width: 960px;
margin: 0;
}
}
.navigation {
margin-top: 30%;
width: 100%;
@media screen and (min-width: 40em) {
margin: 0;
}
@media screen and (min-width: 62em) {
width: 960px;
margin: 0;
}
}
Another feature of Less that we have already seen is its support for mathematical operations, allowing style
attributes to be constructed from pre-defined variables. This makes updating related styles much easier, since the
base variable can be modified and all dependent values change automatically.
CSS files, especially for large sites (and especially if media queries are being used), tend to get quite large over
time, making working with them unwieldy. Less files can be defined separately, then pulled together using @import
directives. Less can also be used to import individual CSS files, as well, if desired.
Mixins can accept parameters, and Less supports conditional logic in the form of mixin guards, which provide a
declarative way to define when certain mixins take effect. A common use for mixin guards is to adjust colors based
on how light or dark the source color is. Given a mixin that accepts a parameter for color, a mixin guard can be
used to modify the mixin based on that color:
.box (@color) when (lightness(@color) >= 50%) {
background-color: #000;
}
.box (@color) when (lightness(@color) < 50%) {
background-color: #FFF;
}
.box (@color) {
color: @color;
}
.feature {
.box (@base);
}
Given our current @base value of #663333 , this Less script will produce the following CSS:
.feature {
background-color: #FFF;
color: #663333;
}
Less provides a number of additional features, but this should give you some idea of the power of this
preprocessing language.
Sass
Sass is similar to Less, providing support for many of the same features, but with slightly different syntax. It's built
using Ruby, rather than JavaScript, and so has different setup requirements. The original Sass language didn't use
curly braces or semicolons, but instead defined scope using white space and indentation. In version 3 of Sass, a
new syntax was introduced, SCSS ("Sassy CSS"). SCSS is similar to CSS in that it ignores indentation levels and
whitespace, and instead uses semicolons and curly braces.
To install Sass, typically you would first install Ruby (pre-installed on macOS ), and then run:
However, if you're running Visual Studio, you can get started with Sass in much the same way as you would with
Less. Open package.json and add the "gulp-sass" package to devDependencies :
"devDependencies": {
"gulp": "3.9.1",
"gulp-less": "3.3.0",
"gulp-sass": "3.1.0"
}
Next, modify gulpfile.js to add a sass variable and a task to compile your Sass files and place the results in the
wwwroot folder:
var gulp = require("gulp"),
fs = require("fs"),
less = require("gulp-less"),
sass = require("gulp-sass");
gulp.task("sass", function () {
return gulp.src('Styles/main2.scss')
.pipe(sass())
.pipe(gulp.dest('wwwroot/css'));
});
Now you can add the Sass file main2.scss to the Styles folder in the root of the project:
$base: #CC0000;
body {
background-color: $base;
}
Save all of your files. Now when you refresh Task Runner Explorer, you see a sass task. Run it, and look in the
/wwwroot/css folder. There's now a main2.css file, with these contents:
body {
background-color: #CC0000;
}
Sass supports nesting in much the same was that Less does, providing similar benefits. Files can be split up by
function and included using the @import directive:
@import 'anotherfile';
Sass supports mixins as well, using the @mixin keyword to define them and @include to include them, as in this
example from sass-lang.com:
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
border-radius: $radius;
}
In addition to mixins, Sass also supports the concept of inheritance, allowing one class to extend another. It's
conceptually similar to a mixin, but results in less CSS code. It's accomplished using the @extend keyword. To try
out mixins, add the following to your main2.scss file:
@mixin alert {
border: 1px solid black;
padding: 5px;
color: #333333;
}
.success {
@include alert;
border-color: green;
}
.error {
@include alert;
color: red;
border-color: red;
font-weight:bold;
}
Examine the output in main2.css after running the sass task in Task Runner Explorer:
.success {
border: 1px solid black;
padding: 5px;
color: #333333;
border-color: green;
}
.error {
border: 1px solid black;
padding: 5px;
color: #333333;
color: red;
border-color: red;
font-weight: bold;
}
Notice that all of the common properties of the alert mixin are repeated in each class. The mixin did a good job of
helping eliminate duplication at development time, but it's still creating CSS with a lot of duplication in it, resulting
in larger than necessary CSS files - a potential performance issue.
Now replace the alert mixin with a .alert class, and change @include to @extend (remembering to extend
.alert , not alert ):
.alert {
border: 1px solid black;
padding: 5px;
color: #333333;
}
.success {
@extend .alert;
border-color: green;
}
.error {
@extend .alert;
color: red;
border-color: red;
font-weight:bold;
}
.success {
border-color: green;
}
.error {
color: red;
border-color: red;
font-weight: bold;
}
Now the properties are defined only as many times as needed, and better CSS is generated.
Sass also includes functions and conditional logic operations, similar to Less. In fact, the two languages' capabilities
are very similar.
Less or Sass?
There's still no consensus as to whether it's generally better to use Less or Sass (or even whether to prefer the
original Sass or the newer SCSS syntax within Sass). Probably the most important decision is to use one of these
tools, as opposed to just hand-coding your CSS files. Once you've made that decision, both Less and Sass are
good choices.
Font Awesome
In addition to CSS preprocessors, another great resource for styling modern web applications is Font Awesome.
Font Awesome is a toolkit that provides over 500 scalable vector icons that can be freely used in your web
applications. It was originally designed to work with Bootstrap, but it has no dependency on that framework or on
any JavaScript libraries.
The easiest way to get started with Font Awesome is to add a reference to it, using its public content delivery
network (CDN ) location:
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
You can also add it to your Visual Studio project by adding it to the "dependencies" in bower.json:
{
"name": "ASP.NET",
"private": true,
"dependencies": {
"bootstrap": "3.0.0",
"jquery": "1.10.2",
"jquery-validation": "1.11.1",
"jquery-validation-unobtrusive": "3.2.2",
"hammer.js": "2.0.4",
"bootstrap-touch-carousel": "0.8.0",
"Font-Awesome": "4.3.0"
}
}
Once you have a reference to Font Awesome on a page, you can add icons to your application by applying Font
Awesome classes, typically prefixed with "fa-", to your inline HTML elements (such as <span> or <i> ). For
example, you can add icons to simple lists and menus using code like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link href="lib/font-awesome/css/font-awesome.css" rel="stylesheet" />
</head>
<body>
<ul class="fa-ul">
<li><i class="fa fa-li fa-home"></i> Home</li>
<li><i class="fa fa-li fa-cog"></i> Settings</li>
</ul>
</body>
</html>
This produces the following in the browser - note the icon beside each item:
Summary
Modern web applications increasingly demand responsive, fluid designs that are clean, intuitive, and easy to use
from a variety of devices. Managing the complexity of the CSS stylesheets required to achieve these goals is best
done using a preprocessor like Less or Sass. In addition, toolkits like Font Awesome quickly provide well-known
icons to textual navigation menus and buttons, improving the overall user experience of your application.
Bundle and minify static assets in ASP.NET Core
9/24/2018 • 10 minutes to read • Edit Online
By Scott Addie
This article explains the benefits of applying bundling and minification, including how these features can be used
with ASP.NET Core web apps.
AddAltToImg=function(n,t){var i=$(n,t);i.attr("alt",i.attr("id").replace(/ID/,""))};
In addition to removing the comments and unnecessary whitespace, the following parameter and variable names
were renamed as follows:
ORIGINAL RENAMED
imageTagAndImageID t
imageContext a
imageElement r
Browsers are fairly verbose with regard to HTTP request headers. The total bytes sent metric saw a significant
reduction when bundling. The load time shows a significant improvement, however this example ran locally.
Greater performance gains are realized when using bundling and minification with assets transferred over a
network.
NOTE
BuildBundlerMinifier belongs to a community-driven project on GitHub for which Microsoft provides no support. Issues
should be filed here.
Visual Studio
.NET Core CLI
Add the BuildBundlerMinifier package to your project.
Build the project. The following appears in the Output window:
1>------ Build started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>
1>Bundler: Begin processing bundleconfig.json
1> Minified wwwroot/css/site.min.css
1> Minified wwwroot/js/site.min.js
1>Bundler: Done processing bundleconfig.json
1>BuildBundlerMinifierApp -> C:\BuildBundlerMinifierApp\bin\Debug\netcoreapp2.0\BuildBundlerMinifierApp.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
1>------ Clean started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>
1>Bundler: Cleaning output from bundleconfig.json
1>Bundler: Done cleaning output file from bundleconfig.json
========== Clean: 1 succeeded, 0 failed, 0 skipped ==========
NOTE
BundlerMinifier.Core belongs to a community-driven project on GitHub for which Microsoft provides no support. Issues
should be filed here.
This package extends the .NET Core CLI to include the dotnet-bundle tool. The following command can be
executed in the Package Manager Console (PMC ) window or in a command shell:
dotnet bundle
IMPORTANT
NuGet Package Manager adds dependencies to the *.csproj file as <PackageReference /> nodes. The dotnet bundle
command is registered with the .NET Core CLI only when a <DotNetCliToolReference /> node is used. Modify the *.csproj
file accordingly.
footer {
margin-top: 10px;
}
To minify custom.css and bundle it with site.css into a site.min.css file, add the relative path to bundleconfig.json:
[
{
"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css",
"wwwroot/css/custom.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true,
"renameLocals": true
},
"sourceMap": false
}
]
NOTE
Alternatively, the following globbing pattern could be used:
"inputFiles": ["wwwroot/**/*(*.css|!(*.min.css))"]
This globbing pattern matches all CSS files and excludes the minified file pattern.
Build the application. Open site.min.css and notice the content of custom.css is appended to the end of the file.
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
The following environment tag renders the bundled and minified CSS files when running in an environment other
than Development . For example, running in Production or Staging triggers the rendering of these stylesheets:
ASP.NET Core 2.x
ASP.NET Core 1.x
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
NOTE
The Bundler & Minifier extension belongs to a community-driven project on GitHub for which Microsoft provides no
support. Issues should be filed here.
Right-click the bundleconfig.json file in Solution Explorer and select Bundler & Minifier > Convert To Gulp...:
The gulpfile.js and package.json files are added to the project. The supporting npm packages listed in the
package.json file's devDependencies section are installed.
Run the following command in the PMC window to install the Gulp CLI as a global dependency:
npm i -g gulp-cli
The gulpfile.js file reads the bundleconfig.json file for the inputs, outputs, and settings.
"use strict";
"devDependencies": {
"del": "^3.0.0",
"gulp": "^3.9.1",
"gulp-concat": "^2.6.1",
"gulp-cssmin": "^0.2.0",
"gulp-htmlmin": "^3.0.0",
"gulp-uglify": "^3.0.0",
"merge-stream": "^1.0.1"
}
Install the dependencies by running the following command at the same level as package.json:
npm i
npm i -g gulp-cli
"use strict";
var regex = {
css: /\.css$/,
html: /\.(html|htm)$/,
js: /\.js$/
};
gulp.task("min:js", function () {
var tasks = getBundles(regex.js).map(function (bundle) {
return gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName))
.pipe(uglify())
.pipe(gulp.dest("."));
});
return merge(tasks);
});
gulp.task("min:css", function () {
var tasks = getBundles(regex.css).map(function (bundle) {
return gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
return merge(tasks);
return merge(tasks);
});
gulp.task("min:html", function () {
var tasks = getBundles(regex.html).map(function (bundle) {
return gulp.src(bundle.inputFiles, { base: "." })
.pipe(concat(bundle.outputFileName))
.pipe(htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true }))
.pipe(gulp.dest("."));
});
return merge(tasks);
});
gulp.task("clean", function () {
var files = bundleconfig.map(function (bundle) {
return bundle.outputFileName;
});
return del(files);
});
gulp.task("watch", function () {
getBundles(regex.js).forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:js"]);
});
getBundles(regex.css).forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:css"]);
});
getBundles(regex.html).forEach(function (bundle) {
gulp.watch(bundle.inputFiles, ["min:html"]);
});
});
function getBundles(regexPattern) {
return bundleconfig.filter(function (bundle) {
return regexPattern.test(bundle.outputFileName);
});
}
In this example, any tasks defined within the MyPreCompileTarget target run before the predefined Build target.
Output similar to the following appears in Visual Studio's Output window:
1>------ Build started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>BuildBundlerMinifierApp -> C:\BuildBundlerMinifierApp\bin\Debug\netcoreapp2.0\BuildBundlerMinifierApp.dll
1>[14:17:49] Using gulpfile C:\BuildBundlerMinifierApp\gulpfile.js
1>[14:17:49] Starting 'min:js'...
1>[14:17:49] Starting 'min:css'...
1>[14:17:49] Starting 'min:html'...
1>[14:17:49] Finished 'min:js' after 83 ms
1>[14:17:49] Finished 'min:css' after 88 ms
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
Alternatively, Visual Studio's Task Runner Explorer may be used to bind Gulp tasks to specific Visual Studio
events. See Running default tasks for instructions on doing that.
Additional resources
Use Gulp
Use Grunt
Use multiple environments
Tag Helpers
Browser Link in ASP.NET Core
8/16/2018 • 3 minutes to read • Edit Online
install-package Microsoft.VisualStudio.Web.BrowserLink
Alternatively, you can use NuGet Package Manager. Right-click the project name in Solution Explorer and
choose Manage NuGet Packages:
Find and install the package:
Configuration
In the Startup.Configure method:
app.UseBrowserLink();
Usually the code is inside an if block that only enables Browser Link in the Development environment, as shown
here:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
NOTE
Some Visual Studio plug-ins, most notably Web Extension Pack 2015 and Web Extension Pack 2017, offer extended
functionality for Browser Link, but some of the additional features don't work with ASP.NET Core projects.
To open multiple browsers at once, choose Browse with... from the same drop-down. Hold down the CTRL key to
select the browsers you want, and then click Browse:
Here's a screenshot showing Visual Studio with the Index view open and two open browsers:
Hover over the Browser Link toolbar control to see the browsers that are connected to the project:
Change the Index view, and all connected browsers are updated when you click the Browser Link refresh button:
Browser Link also works with browsers that you launch from outside Visual Studio and navigate to the application
URL.
The Browser Link Dashboard
Open the Browser Link Dashboard from the Browser Link drop down menu to manage the connection with open
browsers:
If no browser is connected, you can start a non-debugging session by selecting the View in Browser link:
Otherwise, the connected browsers are shown with the path to the page that each browser is showing:
If you like, you can click on a listed browser name to refresh that single browser.
Enable or disable Browser Link
When you re-enable Browser Link after disabling it, you must refresh the browsers to reconnect them.
Enable or disable CSS Auto -Sync
When CSS Auto-Sync is enabled, connected browsers are automatically refreshed when you make any change to
CSS files.
How it works
Browser Link uses SignalR to create a communication channel between Visual Studio and the browser. When
Browser Link is enabled, Visual Studio acts as a SignalR server that multiple clients (browsers) can connect to.
Browser Link also registers a middleware component in the ASP.NET Core request pipeline. This component
injects special <script> references into every page request from the server. You can see the script references by
selecting View source in the browser and scrolling to the end of the <body> tag content:
Your source files aren't modified. The middleware component injects the script references dynamically.
Because the browser-side code is all JavaScript, it works on all browsers that SignalR supports without requiring a
browser plug-in.
Use JavaScriptServices to Create Single Page
Applications in ASP.NET Core
9/21/2018 • 11 minutes to read • Edit Online
What is JavaScriptServices
JavaScriptServices is a collection of client-side technologies for ASP.NET Core. Its goal is to position ASP.NET
Core as developers' preferred server-side platform for building SPAs.
JavaScriptServices consists of three distinct NuGet packages:
Microsoft.AspNetCore.NodeServices (NodeServices)
Microsoft.AspNetCore.SpaServices (SpaServices)
Microsoft.AspNetCore.SpaTemplates (SpaTemplates)
These packages are useful if you:
Run JavaScript on the server
Use a SPA framework or library
Build client-side assets with Webpack
Much of the focus in this article is placed on using the SpaServices package.
What is SpaServices
SpaServices was created to position ASP.NET Core as developers' preferred server-side platform for building
SPAs. SpaServices isn't required to develop SPAs with ASP.NET Core, and it doesn't lock you into a particular
client framework.
SpaServices provides useful infrastructure such as:
Server-side prerendering
Webpack Dev Middleware
Hot Module Replacement
Routing helpers
Collectively, these infrastructure components enhance both the development workflow and the runtime
experience. The components can be adopted individually.
Note: If you're deploying to an Azure web site, you don't need to do anything here — Node.js is installed and
available in the server environments.
.NET Core SDK 2.0 or later
If you're on Windows using Visual Studio 2017, the SDK is installed by selecting the .NET Core cross-
platform development workload.
Microsoft.AspNetCore.SpaServices NuGet package
Server-side prerendering
A universal (also known as isomorphic) application is a JavaScript application capable of running both on the
server and the client. Angular, React, and other popular frameworks provide a universal platform for this
application development style. The idea is to first render the framework components on the server via Node.js, and
then delegate further execution to the client.
ASP.NET Core Tag Helpers provided by SpaServices simplify the implementation of server-side prerendering by
invoking the JavaScript functions on the server.
Prerequisites
Install the following:
aspnet-prerendering npm package:
npm i -S aspnet-prerendering
Configuration
The Tag Helpers are made discoverable via namespace registration in the project's _ViewImports.cshtml file:
@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
These Tag Helpers abstract away the intricacies of communicating directly with low -level APIs by leveraging an
HTML -like syntax inside the Razor view:
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
In the following Angular example, the ClientApp/boot-server.ts file utilizes the createServerRenderer function and
RenderResult type of the aspnet-prerendering npm package to configure server rendering via Node.js. The HTML
markup destined for server-side rendering is passed to a resolve function call, which is wrapped in a strongly-
typed JavaScript Promise object. The Promise object's significance is that it asynchronously supplies the HTML
markup to the page for injection in the DOM's placeholder element.
<app asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data='new {
UserName = "John Doe"
}'>Loading...</app>
The received UserName argument is serialized using the built-in JSON serializer and is stored in the params.data
object. In the following Angular example, the data is used to construct a personalized greeting within an h1
element:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
Note: Property names passed in Tag Helpers are represented with PascalCase notation. Contrast that to
JavaScript, where the same property names are represented with camelCase. The default JSON serialization
configuration is responsible for this difference.
To expand upon the preceding code example, data can be passed from the server to the view by hydrating the
globals property provided to the resolve function:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
The postList array defined inside the globals object is attached to the browser's global window object. This
variable hoisting to global scope eliminates duplication of effort, particularly as it pertains to loading the same
data once on the server and again on the client.
Prerequisites
Install the following:
aspnet-webpack npm package:
npm i -D aspnet-webpack
Configuration
Webpack Dev Middleware is registered into the HTTP request pipeline via the following code in the Startup.cs file's
Configure method:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
The extension method must be called before registering static file hosting via the
UseWebpackDevMiddleware
UseStaticFiles extension method. For security reasons, register the middleware only when the app runs in
development mode.
The webpack.config.js file's output.publicPath property tells the middleware to watch the dist folder for
changes:
Configuration
The HMR component must be registered into MVC's HTTP request pipeline in the Configure method:
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});
As was true with Webpack Dev Middleware, the UseWebpackDevMiddleware extension method must be called before
the UseStaticFiles extension method. For security reasons, register the middleware only when the app runs in
development mode.
The webpack.config.js file must define a plugins array, even if it's left empty:
After loading the app in the browser, the developer tools' Console tab provides confirmation of HMR activation:
Routing helpers
In most ASP.NET Core-based SPAs, you'll want client-side routing in addition to server-side routing. The SPA and
MVC routing systems can work independently without interference. There's, however, one edge case posing
challenges: identifying 404 HTTP responses.
Consider the scenario in which an extensionless route of /some/page is used. Assume the request doesn't pattern-
match a server-side route, but its pattern does match a client-side route. Now consider an incoming request for
/images/user-512.png , which generally expects to find an image file on the server. If that requested resource path
doesn't match any server-side route or static file, it's unlikely that the client-side application would handle it — you
generally want to return a 404 HTTP status code.
Prerequisites
Install the following:
The client-side routing npm package. Using Angular as an example:
npm i -S @angular/router
Configuration
An extension method named MapSpaFallbackRoute is used in the Configure method:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
Tip: Routes are evaluated in the order in which they're configured. Consequently, the default route in the
preceding code example is used first for pattern matching.
To create a new project using one of the SPA templates, include the Short Name of the template in the dotnet
new command. The following command creates an Angular application with ASP.NET Core MVC configured for
the server side:
dotnet run
The application starts on localhost according to the runtime configuration mode. Navigating to
http://localhost:5000 in the browser displays the landing page.
it('should start with count 0, then increments by 1 when clicked', async(() => {
const countElement = fixture.nativeElement.querySelector('strong');
expect(countElement.textContent).toEqual('0');
Open the command prompt in the ClientApp directory. Run the following command:
npm test
The script launches the Karma test runner, which reads the settings defined in the karma.conf.js file. Among other
settings, the karma.conf.js identifies the test files to be executed via its files array:
Additional resources
Angular Docs
Use the Single Page Application templates with
ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
NOTE
The released .NET Core 2.0.x SDK includes older project templates for Angular, React, and React with Redux. This
documentation isn't about those older project templates. This documentation is for the latest Angular, React, and React with
Redux templates, which can be installed manually into ASP.NET Core 2.0. The templates are included by default with ASP.NET
Core 2.1.
Prerequisites
.NET Core SDK 2.0 or later
Node.js, version 6 or later
Installation
The templates are already installed with ASP.NET Core 2.1.
If you have ASP.NET Core 2.0, run the following command to install the updated ASP.NET Core templates for
Angular, React, and React with Redux:
NOTE
This documentation isn't about the Angular project template included in ASP.NET Core 2.0. It's about the newer Angular
template to which you can update manually. The template is included in ASP.NET Core 2.1 by default.
The updated Angular project template provides a convenient starting point for ASP.NET Core apps using Angular
and the Angular CLI to implement a rich, client-side user interface (UI).
The template is equivalent to creating an ASP.NET Core project to act as an API backend and an Angular CLI
project to act as a UI. The template offers the convenience of hosting both project types in a single app project.
Consequently, the app project can be built and published as a single unit.
Run the app from either Visual Studio or the .NET Core CLI:
Visual Studio
.NET Core CLI
Open the generated .csproj file, and run the app as normal from there.
The build process restores npm dependencies on the first run, which can take several minutes. Subsequent builds
are much faster.
The project template creates an ASP.NET Core app and an Angular app. The ASP.NET Core app is intended to be
used for data access, authorization, and other server-side concerns. The Angular app, residing in the ClientApp
subdirectory, is intended to be used for all UI concerns.
Run ng commands
In a command prompt, switch to the ClientApp subdirectory:
cd ClientApp
If you have the ng tool installed globally, you can run any of its commands. For example, you can run ng lint ,
ng test , or any of the other Angular CLI commands. There's no need to run ng serve though, because your
ASP.NET Core app deals with serving both server-side and client-side parts of your app. Internally, it uses
ng serve in development.
If you don't have the ng tool installed, run npm run ng instead. For example, you can run npm run ng lint or
npm run ng test .
cd ClientApp
npm install --save <package_name>
cd ClientApp
npm start
IMPORTANT
Use npm start to launch the Angular CLI development server, not ng serve , so that the configuration in
package.json is respected. To pass additional parameters to the Angular CLI server, add them to the relevant
scripts line in your package.json file.
2. Modify your ASP.NET Core app to use the external Angular CLI instance instead of launching one of its
own. In your Startup class, replace the spa.UseAngularCliServer invocation with the following:
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
When you start your ASP.NET Core app, it won't launch an Angular CLI server. The instance you started manually
is used instead. This enables it to start and restart faster. It's no longer waiting for Angular CLI to rebuild your
client app each time.
Server-side rendering
As a performance feature, you can choose to pre-render your Angular app on the server as well as running it on
the client. This means that browsers receive HTML markup representing your app's initial UI, so they display it
even before downloading and executing your JavaScript bundles. Most of the implementation of this comes from
an Angular feature called Angular Universal.
TIP
Enabling server-side rendering (SSR) introduces a number of extra complications both during development and deployment.
Read drawbacks of SSR to determine if SSR is a good fit for your requirements.
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.UseSpaPrerendering(options =>
{
options.BootModulePath = $"{spa.Options.SourcePath}/dist-server/main.bundle.js";
options.BootModuleBuilder = env.IsDevelopment()
? new AngularCliBuilder(npmScript: "build:ssr")
: null;
options.ExcludeUrls = new[] { "/sockjs-node" };
});
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
In development mode, this code attempts to build the SSR bundle by running the script build:ssr , which is
defined in ClientApp\package.json. This builds an Angular app named ssr , which isn't yet defined.
At the end of the apps array in ClientApp/.angular-cli.json, define an extra app with name ssr . Use the following
options:
{
"name": "ssr",
"root": "src",
"outDir": "dist-server",
"assets": [
"assets"
],
"main": "main.server.ts",
"tsconfig": "tsconfig.server.json",
"prefix": "app",
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
},
"platform": "server"
}
This new SSR -enabled app configuration requires two further files: tsconfig.server.json and main.server.ts. The
tsconfig.server.json file specifies TypeScript compilation options. The main.server.ts file serves as the code entry
point during SSR.
Add a new file called tsconfig.server.json inside ClientApp/src (alongside the existing tsconfig.app.json), containing
the following:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"module": "commonjs"
},
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}
This file configures Angular's AoT compiler to look for a module called app.server.module . Add this by creating a
new file at ClientApp/src/app/app.server.module.ts (alongside the existing app.module.ts) containing the following:
@NgModule({
imports: [AppModule, ServerModule, ModuleMapLoaderModule],
bootstrap: [AppComponent]
})
export class AppServerModule { }
This module inherits from your client-side app.module and defines which extra Angular modules are available
during SSR.
Recall that the new ssr entry in .angular-cli.json referenced an entry point file called main.server.ts. You haven't
yet added that file, and now is time to do so. Create a new file at ClientApp/src/main.server.ts (alongside the
existing main.ts), containing the following:
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModule, renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { createServerRenderer } from 'aspnet-prerendering';
export { AppServerModule } from './app/app.server.module';
enableProdMode();
const options = {
document: params.data.originalHtml,
url: params.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP),
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl }
]
};
This file's code is what ASP.NET Core executes for each request when it runs the UseSpaPrerendering middleware
that you added to the Startup class. It deals with receiving params from the .NET code (such as the URL being
requested), and making calls to Angular SSR APIs to get the resulting HTML.
Strictly-speaking, this is sufficient to enable SSR in development mode. It's essential to make one final change so
that your app works correctly when published. In your app's main .csproj file, set the BuildServerSideRenderer
property value to true :
This configures the build process to run build:ssr during publishing and deploy the SSR files to the server. If you
don't enable this, SSR fails in production.
When your app runs in either development or production mode, the Angular code pre-renders as HTML on the
server. The client-side code executes as normal.
Pass data from .NET code into TypeScript code
During SSR, you might want to pass per-request data from your ASP.NET Core app into your Angular app. For
example, you could pass cookie information or something read from a database. To do this, edit your Startup class.
In the callback for UseSpaPrerendering , set a value for options.SupplyData such as the following:
NOTE
This documentation isn't about the React project template included in ASP.NET Core 2.0. It's about the newer React template
to which you can update manually. The template is included in ASP.NET Core 2.1 by default.
The updated React project template provides a convenient starting point for ASP.NET Core apps using React and
create-react-app (CRA) conventions to implement a rich, client-side user interface (UI).
The template is equivalent to creating both an ASP.NET Core project to act as an API backend, and a standard
CRA React project to act as a UI, but with the convenience of hosting both in a single app project that can be built
and published as a single unit.
Run the app from either Visual Studio or the .NET Core CLI:
Visual Studio
.NET Core CLI
Open the generated .csproj file, and run the app as normal from there.
The build process restores npm dependencies on the first run, which can take several minutes. Subsequent builds
are much faster.
The project template creates an ASP.NET Core app and a React app. The ASP.NET Core app is intended to be
used for data access, authorization, and other server-side concerns. The React app, residing in the ClientApp
subdirectory, is intended to be used for all UI concerns.
cd ClientApp
npm start
2. Modify your ASP.NET Core app to use the external CRA server instance instead of launching one of its
own. In your Startup class, replace the spa.UseReactDevelopmentServer invocation with the following:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
When you start your ASP.NET Core app, it won't launch a CRA server. The instance you started manually is used
instead. This enables it to start and restart faster. It's no longer waiting for your React app to rebuild each time.
Use the React-with-Redux project template with
ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
NOTE
This documentation isn't about the React-with-Redux project template included in ASP.NET Core 2.0. It's about the newer
React-with-Redux template to which you can update manually. The template is included in ASP.NET Core 2.1 by default.
The updated React-with-Redux project template provides a convenient starting point for ASP.NET Core apps
using React, Redux, and create-react-app (CRA) conventions to implement a rich, client-side user interface (UI).
With the exception of the project creation command, all information about the React-with-Redux template is the
same as the React template. To create this project type, run dotnet new reactredux instead of dotnet new react .
For more information about the functionality common to both React-based templates, see React template
documentation.
Mobile development with ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
By Steve Smith
Mobile apps can easily communicate with ASP.NET Core backend services.
View or download sample backend services code
Features
The ToDoRest app supports listing, adding, deleting, and updating To-Do items. Each item has an ID, a Name,
Notes, and a property indicating whether it's been Done yet.
The main view of the items, as shown above, lists each item's name and indicates if it's done with a checkmark.
Tapping the + icon opens an add item dialog:
Tapping an item on the main list screen opens up an edit dialog where the item's Name, Notes, and Done settings
can be modified, or the item can be deleted:
This sample is configured by default to use backend services hosted at developer.xamarin.com, which allow read-
only operations. To test it out yourself against the ASP.NET Core app created in the next section running on your
computer, you'll need to update the app's RestUrl constant. Navigate to the ToDoREST project and open the
Constants.cs file. Replace the RestUrl with a URL that includes your machine's IP address (not localhost or
127.0.0.1, since this address is used from the device emulator, not from your machine). Include the port number as
well (5000). In order to test that your services work with a device, ensure you don't have an active firewall
blocking access to this port.
NOTE
Make sure you run the application directly, rather than behind IIS Express, which ignores non-local requests by default. Run
dotnet run from a command prompt, or choose the application name profile from the Debug Target dropdown in the Visual
Studio toolbar.
Add a model class to represent To-Do items. Mark required fields using the [Required] attribute:
using System.ComponentModel.DataAnnotations;
namespace ToDoApi.Models
{
public class ToDoItem
{
[Required]
public string ID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Notes { get; set; }
The API methods require some way to work with data. Use the same IToDoRepository interface the original
Xamarin sample uses:
using System.Collections.Generic;
using ToDoApi.Models;
namespace ToDoApi.Interfaces
{
public interface IToDoRepository
{
bool DoesItemExist(string id);
IEnumerable<ToDoItem> All { get; }
ToDoItem Find(string id);
void Insert(ToDoItem item);
void Update(ToDoItem item);
void Delete(string id);
}
}
For this sample, the implementation just uses a private collection of items:
using System.Collections.Generic;
using System.Linq;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Services
{
public class ToDoRepository : IToDoRepository
{
private List<ToDoItem> _toDoList;
public ToDoRepository()
{
InitializeData();
}
_toDoList.Add(todoItem1);
_toDoList.Add(todoItem2);
_toDoList.Add(todoItem3);
}
}
}
services.AddSingleton<IToDoRepository,ToDoRepository>();
}
TIP
Learn more about creating web APIs in Build your first Web API with ASP.NET Core MVC and Visual Studio.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ToDoApi.Interfaces;
using ToDoApi.Models;
namespace ToDoApi.Controllers
{
[Route("api/[controller]")]
public class ToDoItemsController : Controller
{
private readonly IToDoRepository _toDoRepository;
This API supports four different HTTP verbs to perform CRUD (Create, Read, Update, Delete) operations on the
data source. The simplest of these is the Read operation, which corresponds to an HTTP GET request.
Reading Items
Requesting a list of items is done with a GET request to the List method. The [HttpGet] attribute on the List
method indicates that this action should only handle GET requests. The route for this action is the route specified
on the controller. You don't necessarily need to use the action name as part of the route. You just need to ensure
each action has a unique and unambiguous route. Routing attributes can be applied at both the controller and
method levels to build up specific routes.
[HttpGet]
public IActionResult List()
{
return Ok(_toDoRepository.All);
}
The List method returns a 200 OK response code and all of the ToDo items, serialized as JSON.
You can test your new API method using a variety of tools, such as Postman, shown here:
Creating Items
By convention, creating new data items is mapped to the HTTP POST verb. The Create method has an
[HttpPost] attribute applied to it, and accepts a ToDoItem instance. Since the item argument will be passed in
the body of the POST, this parameter is decorated with the [FromBody] attribute.
Inside the method, the item is checked for validity and prior existence in the data store, and if no issues occur, it's
added using the repository. Checking ModelState.IsValid performs model validation, and should be done in
every API method that accepts user input.
[HttpPost]
public IActionResult Create([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _toDoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
}
_toDoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}
The sample uses an enum containing error codes that are passed to the mobile client:
Test adding new items using Postman by choosing the POST verb providing the new object in JSON format in the
Body of the request. You should also add a request header specifying a Content-Type of application/json .
The method returns the newly created item in the response.
Updating Items
Modifying records is done using HTTP PUT requests. Other than this change, the Edit method is almost
identical to Create . Note that if the record isn't found, the Edit action will return a NotFound (404) response.
[HttpPut]
public IActionResult Edit([FromBody] ToDoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _toDoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}
To test with Postman, change the verb to PUT. Specify the updated object data in the Body of the request.
This method returns a NoContent (204) response when successful, for consistency with the pre-existing API.
Deleting Items
Deleting records is accomplished by making DELETE requests to the service, and passing the ID of the item to be
deleted. As with updates, requests for items that don't exist will receive NotFound responses. Otherwise, a
successful request will get a NoContent (204) response.
[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _toDoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_toDoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}
Note that when testing the delete functionality, nothing is required in the Body of the request.
Additional resources
Authentication and Authorization
Host and deploy ASP.NET Core
9/10/2018 • 3 minutes to read • Edit Online
Publish to a folder
The dotnet publish CLI command compiles app code and copies the files needed to run the app into a publish
folder. When deploying from Visual Studio, the dotnet publish step happens automatically before the files are
copied to the deployment destination.
Folder contents
The publish folder contains .exe and .dll files for the app, its dependencies, and optionally the .NET runtime.
A .NET Core app can be published as self-contained or framework-dependent app. If the app is self-contained,
the .dll files that contain the .NET runtime are included in the publish folder. If the app is framework-dependent,
the .NET runtime files aren't included because the app has a reference to a version of .NET that's installed on the
server. The default deployment model is framework-dependent. For more information, see .NET Core
application deployment.
In addition to .exe and .dll files, the publish folder for an ASP.NET Core app typically contains configuration files,
static assets, and MVC views. For more information, see Directory structure.
Publishing to Azure
See Publish an ASP.NET Core web app to Azure App Service using Visual Studio for instructions on how to
publish an app to Azure using Visual Studio. The app can also be published to Azure from the command line.
Additional resources
For information on using Docker as a hosting environment, see Host ASP.NET Core apps in Docker.
Host ASP.NET Core on Azure App Service
9/10/2018 • 7 minutes to read • Edit Online
Azure App Service is a Microsoft cloud computing platform service for hosting web apps, including ASP.NET
Core.
Useful resources
The Azure Web Apps Documentation is the home for Azure Apps documentation, tutorials, samples, how -to
guides, and other resources. Two notable tutorials that pertain to hosting ASP.NET Core apps are:
Quickstart: Create an ASP.NET Core web app in Azure
Use Visual Studio to create and deploy an ASP.NET Core web app to Azure App Service on Windows.
Quickstart: Create a .NET Core web app in App Service on Linux
Use the command line to create and deploy an ASP.NET Core web app to Azure App Service on Linux.
The following articles are available in ASP.NET Core documentation:
Publish to Azure with Visual Studio
Learn how to publish an ASP.NET Core app to Azure App Service using Visual Studio.
Publish to Azure with CLI tools
Learn how to publish an ASP.NET Core app to Azure App Service using the Git command-line client.
Continuous deployment to Azure with Visual Studio and Git
Learn how to create an ASP.NET Core web app using Visual Studio and deploy it to Azure App Service using Git
for continuous deployment.
Create your first pipeline with Azure Pipelines
Set up a CI build for an ASP.NET Core app, then create a continuous deployment release to Azure App Service.
Azure Web App sandbox
Discover Azure App Service runtime execution limitations enforced by the Azure Apps platform.
Application configuration
In ASP.NET Core 2.0 or later, the following NuGet packages provide automatic logging features for apps deployed
to Azure App Service:
Microsoft.AspNetCore.AzureAppServices.HostingStartup uses IHostingStartup to provide ASP.NET Core
light-up integration with Azure App Service. The added logging features are provided by the
Microsoft.AspNetCore.AzureAppServicesIntegration package.
Microsoft.AspNetCore.AzureAppServicesIntegration executes AddAzureWebAppDiagnostics to add Azure
App Service diagnostics logging providers in the Microsoft.Extensions.Logging.AzureAppServices package.
Microsoft.Extensions.Logging.AzureAppServices provides logger implementations to support Azure App
Service diagnostics logs and log streaming features.
If targeting .NET Core and referencing the Microsoft.AspNetCore.All metapackage, the packages are already
included. The packages are absent from the newer Microsoft.AspNetCore.App metapackage. If targeting .NET
Framework or referencing the Microsoft.AspNetCore.App metapackage, reference the individual logging packages.
Override app configuration using the Azure Portal
The App Settings area of the Application settings blade permits you to set environment variables for the app.
Environment variables can be consumed by the Environment Variables Configuration Provider.
When an app uses the Web Host and builds the host using WebHost.CreateDefaultBuilder, environment variables
that configure the host use the ASPNETCORE_ prefix. For more information, see ASP.NET Core Web Host and the
Environment Variables Configuration Provider.
When an app uses the Generic Host, environment variables aren't loaded into an app's configuration by default
and the configuration provider must be added by the developer. The developer determines the environment
variable prefix when the configuration provider is added. For more information, see .NET Generic Host and the
Environment Variables Configuration Provider.
Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.<x.y>.x86\
If the installed preview runtime is for ASP.NET Core 2.2, the command is:
Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.2.2.x86\
The command returns True when the x64 preview runtime is installed.
NOTE
The platform architecture (x86/x64) of an App Services app is set in the Application Settings blade under General Settings
for apps that are hosted on an A-series compute or better hosting tier. If the app is run in in-process mode and the platform
architecture is configured for 64-bit (x64), the ASP.NET Core Module uses the 64-bit preview runtime, if present. Install the
ASP.NET Core <x.y> (x64) Runtime extension (for example, ASP.NET Core 2.2 (x64) Runtime).
After installing the x64 preview runtime, run the following command in the Kudu PowerShell command window to verify the
installation. Substitute the ASP.NET Core runtime version for <x.y> in the command:
Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.<x.y>.x64\
If the installed preview runtime is for ASP.NET Core 2.2, the command is:
Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.2.2.x64\
The command returns True when the x64 preview runtime is installed.
NOTE
ASP.NET Core Extensions enables additional functionality for ASP.NET Core on Azure App Services, such as enabling Azure
logging. The extension is installed automatically when deploying from Visual Studio. If the extension isn't installed, install it
for the app.
{
"type": "siteextensions",
"name": "AspNetCoreRuntime",
"apiVersion": "2015-04-01",
"location": "[resourceGroup().location]",
"properties": {
"version": "[parameters('aspnetcoreVersion')]"
},
"dependsOn": [
"[resourceId('Microsoft.Web/Sites', parameters('siteName'))]"
]
}
Additional resources
Web Apps overview (5-minute overview video)
Azure App Service: The Best Place to Host your .NET Apps (55-minute overview video)
Azure Friday: Azure App Service Diagnostic and Troubleshooting Experience (12-minute video)
Azure App Service diagnostics overview
Host ASP.NET Core in a web farm
Azure App Service on Windows Server uses Internet Information Services (IIS ). The following topics pertain to
the underlying IIS technology:
Host ASP.NET Core on Windows with IIS
ASP.NET Core Module
ASP.NET Core Module configuration reference
IIS modules with ASP.NET Core
Microsoft TechNet Library: Windows Server
Publish an ASP.NET Core app to Azure with Visual
Studio
7/27/2018 • 3 minutes to read • Edit Online
IMPORTANT
ASP.NET Core preview releases with Azure App Service
ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core
preview release, see Deploy ASP.NET Core preview release to Azure App Service.
See Publish to Azure from Visual Studio for Mac if you are working on macOS.
To troubleshoot an App Service deployment issue, see Troubleshoot ASP.NET Core on Azure App Service.
Set up
Open a free Azure account if you don't have one.
The app displays the email used to register the new user and a Log out link.
Deploy the app to Azure
Right-click on the project in Solution Explorer and select Publish....
In the Publish dialog:
Select Microsoft Azure App Service.
Select the gear icon and then select Create Profile.
Select Create Profile.
Create Azure resources
The Create App Service dialog appears:
Enter your subscription.
The App Name, Resource Group, and App Service Plan entry fields are populated. You can keep these
names or change them.
Select the Services tab to create a new database.
Select the green + icon to create a new SQL Database
Select New... on the Configure SQL Database dialog to create a new database.
NOTE
"admin" isn't allowed as the administrator user name.
Select OK.
Visual Studio returns to the Create App Service dialog.
Select Create on the Create App Service dialog.
Visual Studio creates the Web app and SQL Server on Azure. This step can take a few minutes. For information
on the resources created, see Additonal resources.
When deployment completes, select Settings:
@page
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>
Additonal resources
Azure App Service
Azure resource groups
Azure SQL Database
Troubleshoot ASP.NET Core on Azure App Service
Publish an ASP.NET Core app to Azure with
command line tools
9/21/2018 • 3 minutes to read • Edit Online
By Cam Soper
IMPORTANT
ASP.NET Core preview releases with Azure App Service
ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core
preview release, see Deploy ASP.NET Core preview release to Azure App Service.
This tutorial will show you how to build and deploy an ASP.NET Core app to Microsoft Azure App Service using
command line tools. When finished, you'll have a Razor Pages web app built in ASP.NET Core hosted as an Azure
App Service Web App. This tutorial is written using Windows command line tools, but can be applied to macOS
and Linux environments, as well.
In this tutorial, you learn how to:
Create an Azure App Service website using Azure CLI
Deploy an ASP.NET Core app to Azure App Service using the Git command line tool
Prerequisites
To complete this tutorial, you'll need:
A Microsoft Azure subscription
.NET Core SDK 2.0 or later
Git command line client
Before deployment, set the account-level deployment credentials using the following command:
az webapp deployment user set --user-name <desired user name> --password <desired password>
A deployment URL is needed to deploy the app using Git. Retrieve the URL like this.
NOTE
It's safe to ignore any warnings from Git about line endings.
Windows
Other
Git prompts for the deployment credentials that were set earlier. After authenticating, the app will be pushed to
the remote location, built, and deployed.
Clean up
When finished testing the app and inspecting the code and resources, delete the web app and plan by deleting the
resource group.
Next steps
In this tutorial, you learned how to:
Create an Azure App Service website using Azure CLI
Deploy an ASP.NET Core app to Azure App Service using the Git command line tool
Next, learn to use the command line to deploy an existing web app that uses CosmosDB.
Deploy to Azure from the command line with .NET Core
Continuous deployment to Azure with Visual Studio
and Git with ASP.NET Core
9/10/2018 • 6 minutes to read • Edit Online
By Erik Reitan
IMPORTANT
ASP.NET Core preview releases with Azure App Service
ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core
preview release, see Deploy ASP.NET Core preview release to Azure App Service.
This tutorial shows how to create an ASP.NET Core web app using Visual Studio and deploy it from Visual Studio
to Azure App Service using continuous deployment.
See also Create your first pipeline with Azure Pipelines, which shows how to configure a continuous delivery (CD )
workflow for Azure App Service using Azure DevOps Services. Azure Pipelines (an Azure DevOps Services
service) simplifies setting up a robust deployment pipeline to publish updates for apps hosted in Azure App
Service. The pipeline can be configured from the Azure portal to build, run tests, deploy to a staging slot, and then
deploy to production.
NOTE
To complete this tutorial, a Microsoft Azure account is required. To obtain an account, activate MSDN subscriber benefits or
sign up for a free trial.
Prerequisites
This tutorial assumes the following software is installed:
Visual Studio
.NET Core SDK 2.0 or later
Git for Windows
NOTE
The most recent release of .NET Core is 2.0.
2. After reviewing the running Web app, close the browser and select the "Stop Debugging" icon in the
toolbar of Visual Studio to stop the app.
Also in the Web App blade, select an existing App Service Plan/Location or create a new one. If creating
a new plan, select the pricing tier, location, and other options. For more information on App Service plans,
see Azure App Service plans in-depth overview.
5. Select Create. Azure will provision and start the web app.
Enable Git publishing for the new web app
Git is a distributed version control system that can be used to deploy an Azure App Service web app. Web app
code is stored in a local Git repository, and the code is deployed to Azure by pushing to a remote repository.
1. Log into the Azure Portal.
2. Select App Services to view a list of the app services associated with the Azure subscription.
3. Select the web app created in the previous section of this tutorial.
4. In the Deployment blade, select Deployment options > Choose Source > Local Git Repository.
5. Select OK.
6. If deployment credentials for publishing a web app or other App Service app haven't previously been set
up, set them up now:
Select Settings > Deployment credentials. The Set deployment credentials blade is displayed.
Create a user name and password. Save the password for later use when setting up Git.
Select Save.
7. In the Web App blade, select Settings > Properties. The URL of the remote Git repository to deploy to is
shown under GIT URL.
8. Copy the GIT URL value for later use in the tutorial.
2. In Team Explorer, select the Home (home icon) > Settings > Repository Settings.
3. In the Remotes section of the Repository Settings, select Add. The Add Remote dialog box is displayed.
4. Set the Name of the remote to Azure-SampleApp.
5. Set the value for Fetch to the Git URL that copied from Azure earlier in this tutorial. Note that this is the
URL that ends with .git.
NOTE
As an alternative, specify the remote repository from the Command Window by opening the Command Window,
changing to the project directory, and entering the command. Example:
git remote add Azure-SampleApp https://[email protected]:443/SampleApp.git
6. Select the Home (home icon) > Settings > Global Settings. Confirm that the name and email address
are set. Select Update if required.
7. Select Home > Changes to return to the Changes view.
8. Enter a commit message, such as Initial Push #1 and select Commit. This action creates a commit locally.
NOTE
As an alternative, commit changes from the Command Window by opening the Command Window, changing to
the project directory, and entering the git commands. Example:
git add .
9. Select Home > Sync > Actions > Open Command Prompt. The command prompt opens to the project
directory.
10. Enter the following command in the command window:
git push -u Azure-SampleApp master
11. Enter the Azure deployment credentials password created earlier in Azure.
This command starts the process of pushing the local project files to Azure. The output from the above
command ends with a message that the deployment was successful.
NOTE
If collaboration on the project is required, consider pushing to GitHub before pushing to Azure.
NOTE
As an alternative, push the changes from the Command Window by opening the Command Window, changing to the
project directory, and entering a git command. Example:
git push -u Azure-SampleApp master
View the updated web app in Azure
View the updated web app by selecting Browse from the web app blade in the Azure Portal or by opening a
browser and entering the URL for the web app. Example: http://SampleWebAppDemo.azurewebsites.net
Additional resources
Create your first pipeline with Azure Pipelines
Project Kudu
Troubleshoot ASP.NET Core on Azure App Service
8/22/2018 • 8 minutes to read • Edit Online
By Luke Latham
IMPORTANT
ASP.NET Core preview releases with Azure App Service
ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core
preview release, see Deploy ASP.NET Core preview release to Azure App Service.
This article provides instructions on how to diagnose an ASP.NET Core app startup issue using Azure App
Service's diagnostic tools. For additional troubleshooting advice, see Azure App Service diagnostics overview
and How to: Monitor Apps in Azure App Service in the Azure documentation.
WARNING
Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files
created. Only use stdout logging to troubleshoot app startup problems.
For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For
more information, see third-party logging providers.
Remote debugging
See the following topics:
Remote debugging web apps section of Troubleshoot a web app in Azure App Service using Visual Studio
(Azure documentation)
Remote Debug ASP.NET Core on IIS in Azure in Visual Studio 2017 (Visual Studio documentation)
Application Insights
Application Insights provides telemetry from apps hosted in the Azure App Service, including error logging and
reporting features. Application Insights can only report on errors that occur after the app starts when the app's
logging features become available. For more information, see Application Insights for ASP.NET Core.
Monitoring blades
Monitoring blades provide an alternative troubleshooting experience to the methods described earlier in the
topic. These blades can be used to diagnose 500-series errors.
Confirm that the ASP.NET Core Extensions are installed. If the extensions aren't installed, install them manually:
1. In the DEVELOPMENT TOOLS blade section, select the Extensions blade.
2. The ASP.NET Core Extensions should appear in the list.
3. If the extensions aren't installed, select the Add button.
4. Choose the ASP.NET Core Extensions from the list.
5. Select OK to accept the legal terms.
6. Select OK on the Add extension blade.
7. An informational pop-up message indicates when the extensions are successfully installed.
If stdout logging isn't enabled, follow these steps:
1. In the Azure portal, select the Advanced Tools blade in the DEVELOPMENT TOOLS area. Select the Go→
button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open Debug console and select CMD.
3. Open the folders to the path site > wwwroot and scroll down to reveal the web.config file at the bottom of
the list.
4. Click the pencil icon next to the web.config file.
5. Set stdoutLogEnabled to true and change the stdoutLogFile path to: \\?\%home%\LogFiles\stdout .
6. Select Save to save the updated web.config file.
Proceed to activate diagnostic logging:
1. In the Azure portal, select the Diagnostics logs blade.
2. Select the On switch for Application Logging (Filesystem ) and Detailed error messages. Select the Save
button at the top of the blade.
3. To include failed request tracing, also known as Failed Request Event Buffering (FREB ) logging, select the On
switch for Failed request tracing.
4. Select the Log stream blade, which is listed immediately under the Diagnostics logs blade in the portal.
5. Make a request to the app.
6. Within the log stream data, the cause of the error is indicated.
Important! Be sure to disable stdout logging when troubleshooting is complete. See the instructions in the
ASP.NET Core Module stdout log section.
To view the failed request tracing logs (FREB logs):
1. Navigate to the Diagnose and solve problems blade in the Azure portal.
2. Select Failed Request Tracing Logs from the SUPPORT TOOLS area of the sidebar.
See Failed request traces section of the Enable diagnostics logging for web apps in Azure App Service topic and
the Application performance FAQs for Web Apps in Azure: How do I turn on failed request tracing? for more
information.
For more information, see Enable diagnostics logging for web apps in Azure App Service.
WARNING
Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files
created.
For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more
information, see third-party logging providers.
Additional resources
Introduction to Error Handling in ASP.NET Core
Common errors reference for Azure App Service and IIS with ASP.NET Core
Troubleshoot a web app in Azure App Service using Visual Studio
Troubleshoot HTTP errors of "502 bad gateway" and "503 service unavailable" in your Azure web apps
Troubleshoot slow web app performance issues in Azure App Service
Application performance FAQs for Web Apps in Azure
Azure Web App sandbox (App Service runtime execution limitations)
Azure Friday: Azure App Service Diagnostic and Troubleshooting Experience (12-minute video)
Host ASP.NET Core on Windows with IIS
9/24/2018 • 19 minutes to read • Edit Online
By Luke Latham
HTTP/2 support
HTTP/2 is supported with ASP.NET Core in the following IIS deployment scenarios:
In-process
Windows Server 2016/Windows 10 or later; IIS 10 or later
Target framework: .NET Core 2.2 or later
TLS 1.2 or later connection
Out-of-process
Windows Server 2016/Windows 10 or later; IIS 10 or later
Edge connections use HTTP/2, but the reverse proxy connection to the Kestrel server uses HTTP/1.1.
Target framework: Not applicable to out-of-process deployments, since the HTTP/2 connection is
handled entirely by IIS.
TLS 1.2 or later connection
For an in-process deployment when an HTTP/2 connection is established, HttpRequest.Protocol reports HTTP/2 .
For an out-of-process deployment when an HTTP/2 connection is established, HttpRequest.Protocol reports
HTTP/1.1 .
HTTP/2 is supported for out-of-process deployments that meet the following base requirements:
Windows Server 2016/Windows 10 or later; IIS 10 or later
Edge connections use HTTP/2, but the reverse proxy connection to the Kestrel server uses HTTP/1.1.
Target framework: Not applicable to out-of-process deployments, since the HTTP/2 connection is handled
entirely by IIS.
TLS 1.2 or later connection
If an HTTP/2 connection is established, HttpRequest.Protocol reports HTTP/1.1 .
HTTP/2 is enabled by default. Connections fall back to HTTP/1.1 if an HTTP/2 connection isn't established. For
more information on HTTP/2 configuration with IIS deployments, see HTTP/2 on IIS.
Application configuration
Enable the IISIntegration components
A typical Program.cs calls CreateDefaultBuilder to begin setting up a host. CreateDefaultBuilder configures
Kestrel as the web server and enables IIS integration by configuring the base path and port for the ASP.NET
Core Module:
The ASP.NET Core Module generates a dynamic port to assign to the back-end process. CreateDefaultBuilder
calls the UseIISIntegration method, which picks up the dynamic port and configures Kestrel to listen on
http://localhost:{dynamicPort}/ . This overrides other URL configurations, such as calls to UseUrls or Kestrel's
Listen API. Therefore, calls to UseUrls or Kestrel's Listen API aren't required when using the module. If
UseUrls or Listen is called, Kestrel listens on the port specified when running the app without IIS.
Both UseKestrel and UseIISIntegration are required. Code calling UseIISIntegration doesn't affect code
portability. If the app isn't run behind IIS (for example, the app is run directly on Kestrel), UseIISIntegration
doesn't operate.
The ASP.NET Core Module generates a dynamic port to assign to the back-end process. The UseIISIntegration
method picks up the dynamic port and configures Kestrel to listen on http://locahost:{dynamicPort}/ . This
overrides other URL configurations, such as calls to UseUrls . Therefore, a call to UseUrls isn't required when
using the module. If UseUrls is called, Kestrel listens on the port specified when running the app without IIS.
If UseUrls is called in an ASP.NET Core 1.0 app, call it before calling UseIISIntegration so that the module-
configured port isn't overwritten. This calling order isn't required with ASP.NET Core 1.1 because the module
setting overrides UseUrls .
For more information on hosting, see Host in ASP.NET Core.
IIS options
To configure IIS options, include a service configuration for IISOptions in ConfigureServices. In the following
example, forwarding client certificates to the app to populate HttpContext.Connection.ClientCertificate is
disabled:
services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = false;
});
<Project Sdk="Microsoft.NET.Sdk.Web">
If a web.config file isn't present in the project, the file is created with the correct processPath and arguments to
configure the ASP.NET Core Module and moved to published output.
If a web.config file is present in the project, the file is transformed with the correct processPath and arguments to
configure the ASP.NET Core Module and moved to published output. The transformation doesn't modify IIS
configuration settings in the file.
The web.config file may provide additional IIS configuration settings that control active IIS modules. For
information on IIS modules that are capable of processing requests with ASP.NET Core apps, see the IIS
modules topic.
To prevent the Web SDK from transforming the web.config file, use the <IsTransformWebConfigDisabled>
property in the project file:
<PropertyGroup>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
When disabling the Web SDK from transforming the file, the processPath and arguments should be manually
set by the developer. For more information, see the ASP.NET Core Module configuration reference.
web.config file location
In order to create the reverse proxy between IIS and the Kestrel server, the web.config file must be present at the
content root path (typically the app base path) of the deployed app. This is the same location as the website
physical path provided to IIS. The web.config file is required at the root of the app to enable the publishing of
multiple apps using Web Deploy.
Sensitive files exist on the app's physical path, such as <assembly>.runtimeconfig.json, <assembly>.xml (XML
Documentation comments), and <assembly>.deps.json. When the web.config file is present and and the site
starts normally, IIS doesn't serve these sensitive files if they're requested. If the web.config file is missing,
incorrectly named, or unable to configure the site for normal startup, IIS may serve sensitive files publicly.
The web.config file must be present in the deployment at all times, correctly named, and able to
configure the site for normal start up. Never remove the web.config file from a production
deployment.
IIS configuration
Windows Server operating systems
Enable the Web Server (IIS ) server role and establish role services.
1. Use the Add Roles and Features wizard from the Manage menu or the link in Server Manager. On
the Server Roles step, check the box for Web Server (IIS ).
2. After the Features step, the Role services step loads for Web Server (IIS ). Select the IIS role services
desired or accept the default role services provided.
Windows Authentication (Optional)
To enable Windows Authentication, expand the following nodes: Web Server > Security. Select the
Windows Authentication feature. For more information, see Windows Authentication
<windowsAuthentication> and Configure Windows authentication.
WebSockets (Optional)
WebSockets is supported with ASP.NET Core 1.1 or later. To enable WebSockets, expand the following
nodes: Web Server > Application Development. Select the WebSocket Protocol feature. For more
information, see WebSockets.
3. Proceed through the Confirmation step to install the web server role and services. A server/IIS restart
isn't required after installing the Web Server (IIS ) role.
Windows desktop operating systems
Enable the IIS Management Console and World Wide Web Services.
1. Navigate to Control Panel > Programs > Programs and Features > Turn Windows features on or
off (left side of the screen).
2. Open the Internet Information Services node. Open the Web Management Tools node.
3. Check the box for IIS Management Console.
4. Check the box for World Wide Web Services.
5. Accept the default features for World Wide Web Services or customize the IIS features.
Windows Authentication (Optional)
To enable Windows Authentication, expand the following nodes: World Wide Web Services > Security.
Select the Windows Authentication feature. For more information, see Windows Authentication
<windowsAuthentication> and Configure Windows authentication.
WebSockets (Optional)
WebSockets is supported with ASP.NET Core 1.1 or later. To enable WebSockets, expand the following
nodes: World Wide Web Services > Application Development Features. Select the WebSocket
Protocol feature. For more information, see WebSockets.
6. If the IIS installation requires a restart, restart the system.
IMPORTANT
Only use the stdout log to troubleshoot app startup failures. Never use stdout logging for routine app logging.
There's no limit on log file size or the number of log files created. The app pool must have write access to the
location where the logs are written. All of the folders on the path to the log location must exist. For more
information on the stdout log, see Log creation and redirection. For information on logging in an ASP.NET Core
app, see the Logging topic.
3. In IIS Manager, open the server's node in the Connections panel. Right-click the Sites folder. Select
Add Website from the contextual menu.
4. Provide a Site name and set the Physical path to the app's deployment folder. Provide the Binding
configuration and create the website by selecting OK:
WARNING
Top-level wildcard bindings ( http://*:80/ and http://+:80 ) should not be used. Top-level wildcard bindings
can open up your app to security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host
names rather than wildcards. Subdomain wildcard binding (for example, *.mysub.com ) doesn't have this security
risk if you control the entire parent domain (as opposed to *.com , which is vulnerable). See rfc7230 section-5.4
for more information.
ASP.NET Core runs in a separate process and manages the runtime. ASP.NET Core doesn't rely on
loading the desktop CLR. Setting the .NET CLR version to No Managed Code is optional.
8. Confirm the process model identity has the proper permissions.
If the default identity of the app pool (Process Model > Identity) is changed from
ApplicationPoolIdentity to another identity, verify that the new identity has the required permissions to
access the app's folder, database, and other required resources. For example, the app pool requires read
and write access to folders where the app reads and writes files.
Windows Authentication configuration (Optional)
For more information, see Configure Windows authentication.
Data protection
The ASP.NET Core Data Protection stack is used by several ASP.NET Core middlewares, including middleware
used in authentication. Even if Data Protection APIs aren't called by user code, data protection should be
configured with a deployment script or in user code to create a persistent cryptographic key store. If data
protection isn't configured, the keys are held in memory and discarded when the app restarts.
If the key ring is stored in memory when the app restarts:
All cookie-based authentication tokens are invalidated.
Users are required to sign in again on their next request.
Any data protected with the key ring can no longer be decrypted. This may include CSRF tokens and
ASP.NET Core MVC TempData cookies.
To configure data protection under IIS to persist the key ring, use one of the following approaches:
Create Data Protection Registry Keys
Data protection keys used by ASP.NET Core apps are stored in the registry external to the apps. To
persist the keys for a given app, create registry keys for the app pool.
For standalone, non-webfarm IIS installations, the Data Protection Provision-AutoGenKeys.ps1
PowerShell script (ASP.NET Core 2.2) can be used for each app pool used with an ASP.NET Core app.
This script creates a registry key in the HKLM registry that's accessible only to the worker process
account of the app's app pool. Keys are encrypted at rest using DPAPI with a machine-wide key.
In web farm scenarios, an app can be configured to use a UNC path to store its data protection key ring.
By default, the data protection keys aren't encrypted. Ensure that the file permissions for the network
share are limited to the Windows account the app runs under. An X509 certificate can be used to protect
keys at rest. Consider a mechanism to allow users to upload certificates: Place certificates into the user's
trusted certificate store and ensure they're available on all machines where the user's app runs. See
Configure ASP.NET Core Data Protection for details.
Configure the IIS Application Pool to load the user profile
This setting is in the Process Model section under the Advanced Settings for the app pool. Set Load
User Profile to True . This stores keys under the user profile directory and protects them using DPAPI
with a key specific to the user account used by the app pool.
Use the file system as a key ring store
Adjust the app code to use the file system as a key ring store. Use an X509 certificate to protect the key
ring and ensure the certificate is a trusted certificate. If the certificate is self-signed, place the certificate in
the Trusted Root store.
When using IIS in a web farm:
Use a file share that all machines can access.
Deploy an X509 certificate to each machine. Configure data protection in code.
Set a machine-wide policy for data protection
The data protection system has limited support for setting a default machine-wide policy for all apps that
consume the Data Protection APIs. See the data protection documentation for details.
Sub-application configuration
Sub-apps added under the root app shouldn't include the ASP.NET Core Module as a handler. If the module is
added as a handler in a sub-app's web.config file, a 500.19 Internal Server Error referencing the faulty config file
is received when attempting to browse the sub-app.
The following example shows a published web.config file for an ASP.NET Core sub-app:
When hosting a non-ASP.NET Core sub-app underneath an ASP.NET Core app, explicitly remove the inherited
handler in the sub-app web.config file:
For more information on configuring the ASP.NET Core Module, see the Introduction to ASP.NET Core Module
topic and the ASP.NET Core Module configuration reference.
Application Pools
When hosting multiple websites on a server, we recommend isolating the apps from each other by running each
app in its own app pool. The IIS Add Website dialog defaults to this configuration. When Site name is
provided, the text is automatically transferred to the Application pool textbox. A new app pool is created using
the site name when the site is added.
6. Select OK.
7. Read & execute permissions should be granted by default. Provide additional permissions as needed.
Access can also be granted at a command prompt using the ICACLS tool. Using the DefaultAppPool as an
example, the following command is used:
Additional resources
Introduction to ASP.NET Core
The Official Microsoft IIS Site
Windows Server technical content library
HTTP/2 on IIS
Troubleshoot ASP.NET Core on IIS
8/13/2018 • 7 minutes to read • Edit Online
By Luke Latham
This article provides instructions on how to diagnose an ASP.NET Core app startup issue when hosting with
Internet Information Services (IIS ). The information in this article applies to hosting in IIS on Windows Server
and Windows Desktop.
In Visual Studio, an ASP.NET Core project defaults to IIS Express hosting during debugging. A 502.5 Process
Failure that occurs when debugging locally can be troubleshooted using the advice in this topic.
Additional troubleshooting topics:
Troubleshoot ASP.NET Core on Azure App Service
Although App Service uses the ASP.NET Core Module and IIS to host apps, see the dedicated topic for
instructions specific to App Service.
Handle errors
Discover how to handle errors in ASP.NET Core apps during development on a local system.
Learn to debug using Visual Studio
This topic introduces the features of the Visual Studio debugger.
Debugging with Visual Studio Code
Learn about the debugging support built into Visual Studio Code.
WARNING
Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files
created.
For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more
information, see third-party logging providers.
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
</environmentVariables>
</aspNetCore>
Setting the environment variable for ASPNETCORE_ENVIRONMENT is only recommended for use on staging and
testing servers that aren't exposed to the Internet. Remove the environment variable from the web.config file
after troubleshooting. For information on setting environment variables in web.config, see environmentVariables
child element of aspNetCore.
Remote debugging
See Remote Debug ASP.NET Core on a Remote IIS Computer in Visual Studio 2017 in the Visual Studio
documentation.
Application Insights
Application Insights provides telemetry from apps hosted by IIS, including error logging and reporting features.
Application Insights can only report on errors that occur after the app starts when the app's logging features
become available. For more information, see Application Insights for ASP.NET Core.
TIP
A convenient way to clear package caches is to execute dotnet nuget locals all --clear from a command prompt.
Clearing package caches can also be accomplished by using the nuget.exe tool and executing the command
nuget locals all -clear . nuget.exe isn't a bundled install with the Windows desktop operating system and must be
obtained separately from the NuGet website.
Additional resources
Introduction to Error Handling in ASP.NET Core
Common errors reference for Azure App Service and IIS with ASP.NET Core
ASP.NET Core Module configuration reference
Troubleshoot ASP.NET Core on Azure App Service
ASP.NET Core Module configuration reference
9/18/2018 • 10 minutes to read • Edit Online
When an app is deployed to Azure App Service, the stdoutLogFile path is set to \\?\%home%\LogFiles\stdout .
The path saves stdout logs to the LogFiles folder, which is a location automatically created by the service.
See Sub-application configuration for an important note pertaining to the configuration of web.config files in
sub-apps.
Attributes of the aspNetCore element
ATTRIBUTE DESCRIPTION DEFAULT
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile="\\?\%home%\LogFiles\stdout">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="CONFIG_DIR" value="f:\application_config" />
</environmentVariables>
</aspNetCore>
WARNING
Only set the ASPNETCORE_ENVIRONMENT envirnonment variable to Development on staging and testing servers that
aren't accessible to untrusted networks, such as the Internet.
app_offline.htm
If a file with the name app_offline.htm is detected in the root directory of an app, the ASP.NET Core Module
attempts to gracefully shutdown the app and stop processing incoming requests. If the app is still running after
the number of seconds defined in shutdownTimeLimit , the ASP.NET Core Module kills the running process.
While the app_offline.htm file is present, the ASP.NET Core Module responds to requests by sending back the
contents of the app_offline.htm file. When the app_offline.htm file is removed, the next request starts the app.
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="true"
stdoutLogFile="\\?\%home%\LogFiles\stdout">
</aspNetCore>
See Configuration with web.config for an example of the aspNetCore element in the web.config file.
Prerequisites
Visual Studio for Windows
ASP.NET and web development workload
.NET Core cross-platform development workload
X.509 security certificate
Enable IIS
1. Navigate to Control Panel > Programs > Programs and Features > Turn Windows features on or off (left
side of the screen).
2. Select the Internet Information Services check box.
Configure IIS
IIS must have a website configured with the following:
A host name that matches the app's launch profile URL host name.
Binding for port 443 with an assigned certificate.
For example, the Host name for an added website is set to "localhost" (the launch profile will also use "localhost"
later in this topic). The port is set to "443" (HTTPS ). The IIS Express Development Certificate is assigned to the
website, but any valid certificate works:
If the IIS installation already has a Default Web Site with a host name that matches the app's launch profile URL
host name:
Add a port binding for port 443 (HTTPS ).
Assign a valid certificate to the website.
In an existing project, use HTTPS Redirection Middleware in Startup.Configure by calling the UseHttpsRedirection
extension method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
Alternatively, manually add a launch profile to the launchSettings.json file in the app:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iis": {
"applicationUrl": "https://localhost/WebApplication1",
"sslPort": 0
}
},
"profiles": {
"IIS": {
"commandName": "IIS",
"launchBrowser": true,
"launchUrl": "https://localhost/WebApplication1",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Visual Studio may prompt a restart if not running as an administrator. If prompted, restart Visual Studio.
If an untrusted development certificate is used, the browser may require you to create an exception for the
untrusted certificate.
Additional resources
Host ASP.NET Core on Windows with IIS
Introduction to ASP.NET Core Module
ASP.NET Core Module configuration reference
Enforce HTTPS
IIS modules with ASP.NET Core
8/30/2018 • 5 minutes to read • Edit Online
By Luke Latham
ASP.NET Core apps are hosted by IIS in a reverse proxy configuration. Some of the native IIS modules and all of
the IIS managed modules aren't available to process requests for ASP.NET Core apps. In many cases, ASP.NET
Core offers an alternative to the features of IIS native and managed modules.
Native modules
The table indicates native IIS modules that are functional on reverse proxy requests to ASP.NET Core apps.
CGI No
CgiModule
Server-Side Includes No
ServerSideIncludeModule
†The URL Rewrite Module's isFile and isDirectory match types don't work with ASP.NET Core apps due to
the changes in directory structure.
Managed modules
Managed modules are not functional with hosted ASP.NET Core apps when the app pool's .NET CLR version is
set to No Managed Code. ASP.NET Core offers middleware alternatives in several cases.
AnonymousIdentification
DefaultAuthentication
FileAuthorization
Profile
RoleManager
ScriptModule-4.0
UrlAuthorization
WindowsAuthentication
<configuration>
<system.webServer>
<httpRedirect enabled="false" />
</system.webServer>
</configuration>
For more information on disabling modules with configuration settings, follow the links in the Child Elements
section of IIS <system.webServer>.
Module removal
If opting to remove a module with a setting in web.config, unlock the module and unlock the <modules> section
of web.config first:
1. Unlock the module at the server level. Select the IIS server in the IIS Manager Connections sidebar. Open
the Modules in the IIS area. Select the module in the list. In the Actions sidebar on the right, select
Unlock. Unlock as many modules as you plan to remove from web.config later.
2. Deploy the app without a <modules> section in web.config. If an app is deployed with a web.config
containing the <modules> section without having unlocked the section first in the IIS Manager, the
Configuration Manager throws an exception when attempting to unlock the section. Therefore, deploy the
app without a <modules> section.
3. Unlock the <modules> section of web.config. In the Connections sidebar, select the website in Sites. In
the Management area, open the Configuration Editor. Use the navigation controls to select the
system.webServer/modules section. In the Actions sidebar on the right, select to Unlock the section.
4. At this point, a <modules> section can be added to the web.config file with a <remove> element to
remove the module from the app. Multiple <remove> elements can be added to remove multiple
modules. If web.config changes are made on the server, immediately make the same changes to the
project's web.config file locally. Removing a module this way won't affect the use of the module with other
apps on the server.
<configuration>
<system.webServer>
<modules>
<remove name="MODULE_NAME" />
</modules>
</system.webServer>
</configuration>
An IIS module can also be removed with Appcmd.exe. Provide the MODULE_NAME and APPLICATION_NAME in the
command:
For example, remove the DynamicCompressionModule from the Default Web Site:
%windir%\system32\inetsrv\appcmd.exe delete module DynamicCompressionModule /app.name:"Default Web Site"
The URI Caching Module ( UriCacheModule ) allows IIS to cache website configuration at the URL level. Without
this module, IIS must read and parse configuration on every request, even when the same URL is repeatedly
requested. Parsing the configuration every request results in a significant performance penalty. Although the URI
Caching Module isn't strictly required for a hosted ASP.NET Core app to run, we recommend that the URI
Caching Module be enabled for all ASP.NET Core deployments.
The HTTP Caching Module ( HttpCacheModule ) implements the IIS output cache and also the logic for caching
items in the HTTP.sys cache. Without this module, content is no longer cached in kernel mode, and cache profiles
are ignored. Removing the HTTP Caching Module usually has adverse effects on performance and resource
usage. Although the HTTP Caching Module isn't strictly required for a hosted ASP.NET Core app to run, we
recommend that the HTTP Caching Module be enabled for all ASP.NET Core deployments.
Additional resources
Host on Windows with IIS
Introduction to IIS Architectures: Modules in IIS
IIS Modules Overview
Customizing IIS 7.0 Roles and Modules
IIS <system.webServer>
Host ASP.NET Core in a Windows Service
9/26/2018 • 7 minutes to read • Edit Online
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
</PropertyGroup>
return WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.UseContentRoot(pathToContentRoot)
.UseStartup<Startup>();
}
host.RunAsService();
}
3. Publish the app. Use dotnet publish or a Visual Studio publish profile. When using a Visual Studio, select
the FolderProfile.
To publish the sample app using command-line interface (CLI) tools, run the dotnet publish command at a
command prompt from the project folder. The RID must be specified in the <RuntimeIdenfifier> (or
<RuntimeIdentifiers> ) property of the project file. In the following example, the app is published in Release
configuration for the win7-x64 runtime:
4. Use the sc.exe command-line tool to create the service. The binPath value is the path to the app's
executable, which includes the executable file name. The space between the equal sign and the quote
character at the start of the path is required.
For a service published in the project folder, use the path to the publish folder to create the service. In the
following example:
The project resides in the c:\my_services\AspNetCoreService folder.
The project is published in Release configuration.
The Target Framework Moniker (TFM ) is netcoreapp2.1 .
The Runtime Identifer (RID ) is win7-x64 .
The app executable is named AspNetCoreService.exe.
The service is named MyService.
Example:
IMPORTANT
Make sure the space is present between the binPath= argument and its value.
sc start MyService
sc query MyService
7. When the service is in the RUNNING state and if the service is a web app, browse the app at its path (by
default, http://localhost:5000 , which redirects to https://localhost:5001 when using HTTPS Redirection
Middleware).
For the sample app service, browse the app at http://localhost:5000 .
8. Stop the service with the sc stop <SERVICE_NAME> command.
The following command stops the sample app service:
sc stop MyService
9. After a short delay to stop a service, uninstall the service with the sc delete <SERVICE_NAME> command.
Check the status of the sample app service:
sc query MyService
When the sample app service is in the STOPPED state, use the following command to uninstall the sample
app service:
sc delete MyService
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
builder.UseContentRoot(pathToContentRoot);
}
if (isService)
{
host.RunAsService();
}
else
{
host.Run();
}
}
Because ASP.NET Core configuration requires name-value pairs for command-line arguments, the --console
switch is removed before the arguments are passed to CreateDefaultBuilder.
NOTE
isService isn't passed from Main into CreateWebHostBuilder because the signature of CreateWebHostBuilder must
be CreateWebHostBuilder(string[]) in order for integration testing to work properly.
public static void Main(string[] args)
{
var isService = true;
if (Debugger.IsAttached || args.Contains("--console"))
{
isService = false;
}
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
pathToContentRoot = Path.GetDirectoryName(pathToExe);
}
if (isService)
{
host.RunAsService();
}
else
{
host.Run();
}
}
2. Create an extension method for IWebHost that passes the custom WebHostService to ServiceBase.Run:
public static class WebHostServiceExtensions
{
public static void RunAsCustomService(this IWebHost host)
{
var webHostService = new CustomWebHostService(host);
ServiceBase.Run(webHostService);
}
}
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
var pathToContentRoot = Path.GetDirectoryName(pathToExe);
builder.UseContentRoot(pathToContentRoot);
}
if (isService)
{
host.RunAsCustomService();
}
else
{
host.Run();
}
}
NOTE
isService isn't passed from Main into CreateWebHostBuilder because the signature of
CreateWebHostBuilder must be CreateWebHostBuilder(string[]) in order for integration testing to work
properly.
public static void Main(string[] args)
{
var isService = true;
if (Debugger.IsAttached || args.Contains("--console"))
{
isService = false;
}
if (isService)
{
var pathToExe = Process.GetCurrentProcess().MainModule.FileName;
pathToContentRoot = Path.GetDirectoryName(pathToExe);
}
if (isService)
{
host.RunAsCustomService();
}
else
{
host.Run();
}
}
If the custom WebHostService code requires a service from dependency injection (such as a logger), obtain it from
the IWebHost.Services property:
internal class CustomWebHostService : WebHostService
{
private ILogger _logger;
Configure HTTPS
Specify a Kestrel server HTTPS endpoint configuration.
Additional resources
Kestrel endpoint configuration (includes HTTPS configuration and SNI support)
ASP.NET Core Web Host
Host ASP.NET Core on Linux with Nginx
9/21/2018 • 12 minutes to read • Edit Online
By Sourabh Shirhatti
This guide explains setting up a production-ready ASP.NET Core environment on an Ubuntu 16.04 server. These
instructions likely work with newer versions of Ubuntu, but the instructions haven't been tested with newer
versions.
For information on other Linux distributions supported by ASP.NET Core, see Prerequisites for .NET Core on
Linux.
NOTE
For Ubuntu 14.04, supervisord is recommended as a solution for monitoring the Kestrel process. systemd isn't available on
Ubuntu 14.04. For Ubuntu 14.04 instructions, see the previous version of this topic.
This guide:
Places an existing ASP.NET Core app behind a reverse proxy server.
Sets up the reverse proxy server to forward requests to the Kestrel web server.
Ensures the web app runs on startup as a daemon.
Configures a process management tool to help restart the web app.
Prerequisites
1. Access to an Ubuntu 16.04 server with a standard user account with sudo privilege.
2. Install the .NET Core runtime on the server.
a. Visit the .NET Core All Downloads page.
b. Select the latest non-preview runtime from the list under Runtime.
c. Select and follow the instructions for Ubuntu that match the Ubuntu version of the server.
3. An existing ASP.NET Core app.
The app can also be published as a self-contained deployment if you prefer not to maintain the .NET Core
runtime on the server.
Copy the ASP.NET Core app to the server using a tool that integrates into the organization's workflow (for
example, SCP, SFTP ). It's common to locate web apps under the var directory (for example,
var/aspnetcore/hellomvc).
NOTE
Under a production deployment scenario, a continuous integration workflow does the work of publishing the app and
copying the assets to the server.
NOTE
Either configuration—with or without a reverse proxy server—is a valid and supported hosting configuration for ASP.NET
Core 2.0 or later apps. For more information, see When to use Kestrel with a reverse proxy.
Any component that depends on the scheme, such as authentication, link generation, redirects, and geolocation,
must be placed after invoking the Forwarded Headers Middleware. As a general rule, Forwarded Headers
Middleware should run before other middleware except diagnostics and error handling middleware. This
ordering ensures that the middleware relying on forwarded headers information can consume the header values
for processing.
ASP.NET Core 2.x
ASP.NET Core 1.x
Invoke the UseForwardedHeaders method in Startup.Configure before calling UseAuthentication or similar
authentication scheme middleware. Configure the middleware to forward the X-Forwarded-For and
X-Forwarded-Proto headers:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseAuthentication();
If no ForwardedHeadersOptions are specified to the middleware, the default headers to forward are None .
Only proxies running on localhost (127.0.0.1, [::1]) are trusted by default. If other trusted proxies or networks
within the organization handle requests between the Internet and the web server, add them to the list of
KnownProxies or KnownNetworks with ForwardedHeadersOptions. The following example adds a trusted proxy
server at IP address 10.0.0.100 to the Forwarded Headers Middleware KnownProxies in
Startup.ConfigureServices :
services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});
For more information, see Configure ASP.NET Core to work with proxy servers and load balancers.
Install Nginx
Use apt-get to install Nginx. The installer creates a systemd init script that runs Nginx as daemon on system
startup. Follow the installation instructions for Ubuntu at Nginx: Official Debian/Ubuntu packages.
NOTE
If optional Nginx modules are required, building Nginx from source might be required.
Since Nginx was installed for the first time, explicitly start it by running:
Verify a browser displays the default landing page for Nginx. The landing page is reachable at
http://<server_IP_address>/index.nginx-debian.html .
Configure Nginx
To configure Nginx as a reverse proxy to forward requests to your ASP.NET Core app, modify /etc/nginx/sites-
available/default. Open it in a text editor, and replace the contents with the following:
server {
listen 80;
server_name example.com *.example.com;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
When no server_name matches, Nginx uses the default server. If no default server is defined, the first server in
the configuration file is the default server. As a best practice, add a specific default server which returns a status
code of 444 in your configuration file. A default server configuration example is:
server {
listen 80 default_server;
# listen [::]:80 default_server deferred;
return 444;
}
With the preceding configuration file and default server, Nginx accepts public traffic on port 80 with host header
example.com or *.example.com . Requests not matching these hosts won't get forwarded to Kestrel. Nginx
forwards the matching requests to Kestrel at http://localhost:5000 . See How nginx processes a request for
more information. To change Kestrel's IP/port, see Kestrel: Endpoint configuration.
WARNING
Failure to specify a proper server_name directive exposes your app to security vulnerabilities. Subdomain wildcard binding
(for example, *.example.com ) doesn't pose this security risk if you control the entire parent domain (as opposed to
*.com , which is vulnerable). See rfc7230 section-5.4 for more information.
Once the Nginx configuration is established, run sudo nginx -t to verify the syntax of the configuration files. If
the configuration file test is successful, force Nginx to pick up the changes by running sudo nginx -s reload .
To directly run the app on the server:
1. Navigate to the app's directory.
2. Run the app's executable: ./<app_executable> .
If the app runs on the server but fails to respond over the Internet, check the server's firewall and confirm that
port 80 is open. If using an Azure Ubuntu VM, add a Network Security Group (NSG ) rule that enables inbound
port 80 traffic. There's no need to enable an outbound port 80 rule, as the outbound traffic is automatically
granted when the inbound rule is enabled.
When done testing the app, shut the app down with Ctrl+C at the command prompt.
[Service]
WorkingDirectory=/var/aspnetcore/hellomvc
ExecStart=/usr/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
If the user www -data isn't used by the configuration, the user defined here must be created first and given proper
ownership for files.
Use TimeoutStopSec to configure the duration of time to wait for the app to shut down after it receives the initial
interrupt signal. If the app doesn't shut down in this period, SIGKILL is issued to terminate the app. Provide the
value as unitless seconds (for example, 150 ), a time span value (for example, 2min 30s ), or infinity to disable
the timeout. TimeoutStopSec defaults to the value of DefaultTimeoutStopSec in the manager configuration file
(systemd -system.conf, system.conf.d, systemd -user.conf, user.conf.d). The default timeout for most distributions is
90 seconds.
systemd-escape "<value-to-escape>"
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked
Viewing logs
Since the web app using Kestrel is managed using systemd , all events and processes are logged to a centralized
journal. However, this journal includes all entries for all services and processes managed by systemd . To view the
kestrel-hellomvc.service -specific items, use the following command:
For further filtering, time options such as --since today , --until 1 hour ago or a combination of these can
reduce the amount of entries returned.
Data protection
The ASP.NET Core Data Protection stack is used by several ASP.NET Core middlewares, including
authentication middleware (for example, cookie middleware) and cross-site request forgery (CSRF ) protections.
Even if Data Protection APIs aren't called by user code, data protection should be configured to create a
persistent cryptographic key store. If data protection isn't configured, the keys are held in memory and discarded
when the app restarts.
If the key ring is stored in memory when the app restarts:
All cookie-based authentication tokens are invalidated.
Users are required to sign in again on their next request.
Any data protected with the key ring can no longer be decrypted. This may include CSRF tokens and
ASP.NET Core MVC TempData cookies.
To configure data protection to persist and encrypt the key ring, see:
Key storage providers in ASP.NET Core
Key encryption At rest in ASP.NET Core
WARNING
A firewall will prevent access to the whole system if not configured correctly. Failure to specify the correct SSH port will
effectively lock you out of the system if you are using SSH to connect to it. The default port is 22. For more information,
see the introduction to ufw and the manual.
Securing Nginx
Change the Nginx response name
Edit src/http/ngx_http_header_filter_module.c:
Configure options
Configure the server with additional required modules. Consider using a web app firewall, such as ModSecurity,
to harden the app.
Configure SSL
Configure the server to listen to HTTPS traffic on port 443 by specifying a valid certificate issued by a
trusted Certificate Authority (CA).
Harden the security by employing some of the practices depicted in the following /etc/nginx/nginx.conf
file. Examples include choosing a stronger cipher and redirecting all traffic over HTTP to HTTPS.
Adding an HTTP Strict-Transport-Security (HSTS ) header ensures all subsequent requests made by the
client are over HTTPS only.
Don't add the Strict-Transport-Security header or chose an appropriate max-age if SSL will be disabled in
the future.
Add the /etc/nginx/proxy.conf configuration file:
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffers 32 4k;
Edit the /etc/nginx/nginx.conf configuration file. The example contains both http and server sections in one
configuration file.
http {
include /etc/nginx/proxy.conf;
limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
server_tokens off;
sendfile on;
keepalive_timeout 29; # Adjust to the lowest possible value that makes sense for your use case.
client_body_timeout 10; client_header_timeout 10; send_timeout 10;
upstream hellomvc{
server localhost:5000;
}
server {
listen *:80;
add_header Strict-Transport-Security max-age=15768000;
return 301 https://$host$request_uri;
}
server {
listen *:443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/testCert.crt;
ssl_certificate_key /etc/ssl/certs/testCert.key;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on; #ensure your cert is capable
ssl_stapling_verify on; #ensure your cert is capable
Add the line add_header X-Frame-Options "SAMEORIGIN"; and save the file, then restart Nginx.
MIME-type sniffing
This header prevents most browsers from MIME -sniffing a response away from the declared content type, as the
header instructs the browser not to override the response content type. With the nosniff option, if the server
says the content is "text/html", the browser renders it as "text/html".
Edit the nginx.conf file:
Add the line add_header X-Content-Type-Options "nosniff"; and save the file, then restart Nginx.
Additional resources
Prerequisites for .NET Core on Linux
Nginx: Binary Releases: Official Debian/Ubuntu packages
Configure ASP.NET Core to work with proxy servers and load balancers
NGINX: Using the Forwarded header
Host ASP.NET Core on Linux with Apache
9/21/2018 • 11 minutes to read • Edit Online
By Shayne Boyer
Using this guide, learn how to set up Apache as a reverse proxy server on CentOS 7 to redirect HTTP traffic to an
ASP.NET Core web app running on Kestrel. The mod_proxy extension and related modules create the server's
reverse proxy.
Prerequisites
1. Server running CentOS 7 with a standard user account with sudo privilege.
2. Install the .NET Core runtime on the server.
a. Visit the .NET Core All Downloads page.
b. Select the latest non-preview runtime from the list under Runtime.
c. Select and follow the instructions for CentOS/Oracle.
3. An existing ASP.NET Core app.
The app can also be published as a self-contained deployment if you prefer not to maintain the .NET Core
runtime on the server.
Copy the ASP.NET Core app to the server using a tool that integrates into the organization's workflow (for
example, SCP, SFTP ). It's common to locate web apps under the var directory (for example,
var/aspnetcore/hellomvc).
NOTE
Under a production deployment scenario, a continuous integration workflow does the work of publishing the app and
copying the assets to the server.
NOTE
Either configuration—with or without a reverse proxy server—is a valid and supported hosting configuration for ASP.NET
Core 2.0 or later apps. For more information, see When to use Kestrel with a reverse proxy.
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseAuthentication();
If no ForwardedHeadersOptions are specified to the middleware, the default headers to forward are None .
Only proxies running on localhost (127.0.0.1, [::1]) are trusted by default. If other trusted proxies or networks
within the organization handle requests between the Internet and the web server, add them to the list of
KnownProxies or KnownNetworks with ForwardedHeadersOptions. The following example adds a trusted proxy
server at IP address 10.0.0.100 to the Forwarded Headers Middleware KnownProxies in
Startup.ConfigureServices :
services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});
For more information, see Configure ASP.NET Core to work with proxy servers and load balancers.
Install Apache
Update CentOS packages to their latest stable versions:
Install the Apache web server on CentOS with a single yum command:
Installed:
httpd.x86_64 0:2.4.6-40.el7.centos.4
Complete!
NOTE
In this example, the output reflects httpd.86_64 since the CentOS 7 version is 64 bit. To verify where Apache is installed,
run whereis httpd from a command prompt.
Configure Apache
Configuration files for Apache are located within the /etc/httpd/conf.d/ directory. Any file with the .conf
extension is processed in alphabetical order in addition to the module configuration files in
/etc/httpd/conf.modules.d/ , which contains any configuration files necessary to load modules.
<VirtualHost *:*>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>
<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ServerName www.example.com
ServerAlias *.example.com
ErrorLog ${APACHE_LOG_DIR}hellomvc-error.log
CustomLog ${APACHE_LOG_DIR}hellomvc-access.log common
</VirtualHost>
The VirtualHost block can appear multiple times, in one or more files on a server. In the preceding configuration
file, Apache accepts public traffic on port 80. The domain www.example.com is being served, and the
*.example.com alias resolves to the same website. See Name-based virtual host support for more information.
Requests are proxied at the root to port 5000 of the server at 127.0.0.1. For bi-directional communication,
ProxyPass and ProxyPassReverse are required. To change Kestrel's IP/port, see Kestrel: Endpoint configuration.
WARNING
Failure to specify a proper ServerName directive in the VirtualHost block exposes your app to security vulnerabilities.
Subdomain wildcard binding (for example, *.example.com ) doesn't pose this security risk if you control the entire parent
domain (as opposed to *.com , which is vulnerable). See rfc7230 section-5.4 for more information.
Logging can be configured per VirtualHost using ErrorLog and CustomLog directives. ErrorLog is the location
where the server logs errors, and CustomLog sets the filename and format of log file. In this case, this is where
request information is logged. There's one line for each request.
Save the file and test the configuration. If everything passes, the response should be Syntax [OK] .
Restart Apache:
[Unit]
Description=Example .NET Web API App running on CentOS 7
[Service]
WorkingDirectory=/var/aspnetcore/hellomvc
ExecStart=/usr/local/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=apache
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
If the user apache isn't used by the configuration, the user must be created first and given proper ownership of
files.
Use TimeoutStopSec to configure the duration of time to wait for the app to shut down after it receives the initial
interrupt signal. If the app doesn't shut down in this period, SIGKILL is issued to terminate the app. Provide the
value as unitless seconds (for example, 150 ), a time span value (for example, 2min 30s ), or infinity to disable
the timeout. TimeoutStopSec defaults to the value of DefaultTimeoutStopSec in the manager configuration file
(systemd -system.conf, system.conf.d, systemd -user.conf, user.conf.d). The default timeout for most distributions is
90 seconds.
Some values (for example, SQL connection strings) must be escaped for the configuration providers to read the
environment variables. Use the following command to generate a properly escaped value for use in the
configuration file:
systemd-escape "<value-to-escape>"
With the reverse proxy configured and Kestrel managed through systemd, the web app is fully configured and
can be accessed from a browser on the local machine at http://localhost . Inspecting the response headers, the
Server header indicates that the ASP.NET Core app is served by Kestrel:
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked
Viewing logs
Since the web app using Kestrel is managed using systemd, events and processes are logged to a centralized
journal. However, this journal includes entries for all of the services and processes managed by systemd. To view
the kestrel-hellomvc.service -specific items, use the following command:
For time filtering, specify time options with the command. For example, use --since today to filter for the
current day or --until 1 hour ago to see the previous hour's entries. For more information, see the man page for
journalctl.
Data protection
The ASP.NET Core Data Protection stack is used by several ASP.NET Core middlewares, including
authentication middleware (for example, cookie middleware) and cross-site request forgery (CSRF ) protections.
Even if Data Protection APIs aren't called by user code, data protection should be configured to create a
persistent cryptographic key store. If data protection isn't configured, the keys are held in memory and discarded
when the app restarts.
If the key ring is stored in memory when the app restarts:
All cookie-based authentication tokens are invalidated.
Users are required to sign in again on their next request.
Any data protected with the key ring can no longer be decrypted. This may include CSRF tokens and
ASP.NET Core MVC TempData cookies.
To configure data protection to persist and encrypt the key ring, see:
Key storage providers in ASP.NET Core
Key encryption At rest in ASP.NET Core
Use firewalld to open only the ports needed for the app. In this case, port 80 and 443 are used. The following
commands permanently set ports 80 and 443 to open:
Reload the firewall settings. Check the available services and ports in the default zone. Options are available by
inspecting firewall-cmd -h .
SSL configuration
To configure Apache for SSL, the mod_ssl module is used. When the httpd module was installed, the mod_ssl
module was also installed. If it wasn't installed, use yum to add it to the configuration.
Modify the hellomvc.conf file to enable URL rewriting and secure communication on port 443:
<VirtualHost *:*>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ErrorLog /var/log/httpd/hellomvc-error.log
CustomLog /var/log/httpd/hellomvc-access.log common
SSLEngine on
SSLProtocol all -SSLv2
SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:!RC4+RSA:+HIGH:+MEDIUM:!LOW:!RC4
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</VirtualHost>
NOTE
This example is using a locally-generated certificate. SSLCertificateFile should be the primary certificate file for the domain
name. SSLCertificateKeyFile should be the key file generated when CSR is created. SSLCertificateChainFile should be
the intermediate certificate file (if any) that was supplied by the certificate authority.
Restart Apache:
Add the line Header append X-FRAME-OPTIONS "SAMEORIGIN" . Save the file. Restart Apache.
MIME-type sniffing
The X-Content-Type-Options header prevents Internet Explorer from MIME -sniffing (determining a file's
Content-Type from the file's content). If the server sets the Content-Type header to text/html with the nosniff
option set, Internet Explorer renders the content as text/html regardless of the file's content.
Edit the httpd.conf file:
Add the line Header set X-Content-Type-Options "nosniff" . Save the file. Restart Apache.
Load Balancing
This example shows how to setup and configure Apache on CentOS 7 and Kestrel on the same instance machine.
In order to not have a single point of failure; using mod_proxy_balancer and modifying the VirtualHost would
allow for managing multiple instances of the web apps behind the Apache proxy server.
In the configuration file shown below, an additional instance of the hellomvc app is setup to run on port 5001.
The Proxy section is set with a balancer configuration with two members to load balance byrequests.
<VirtualHost *:*>
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
ProxyPass / balancer://mycluster/
ProxyPassReverse / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5001/
<Proxy balancer://mycluster>
BalancerMember http://127.0.0.1:5000
BalancerMember http://127.0.0.1:5001
ProxySet lbmethod=byrequests
</Proxy>
<Location />
SetHandler balancer
</Location>
ErrorLog /var/log/httpd/hellomvc-error.log
CustomLog /var/log/httpd/hellomvc-access.log common
SSLEngine on
SSLProtocol all -SSLv2
SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:!RC4+RSA:+HIGH:+MEDIUM:!LOW:!RC4
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</VirtualHost>
Rate Limits
Using mod_ratelimit, which is included in the httpd module, the bandwidth of clients can be limited:
The example file limits bandwidth as 600 KB/sec under the root location:
<IfModule mod_ratelimit.c>
<Location />
SetOutputFilter RATE_LIMIT
SetEnv rate-limit 600
</Location>
</IfModule>
Additional resources
Configure ASP.NET Core to work with proxy servers and load balancers
Host ASP.NET Core in Docker containers
6/21/2018 • 2 minutes to read • Edit Online
The following articles are available for learning about hosting ASP.NET Core apps in Docker:
Introduction to Containers and Docker
See how containerization is an approach to software development in which an application or service, its
dependencies, and its configuration are packaged together as a container image. The image can be tested and then
deployed to a host.
What is Docker
Discover how Docker is an open-source project for automating the deployment of apps as portable, self-sufficient
containers that can run on the cloud or on-premises.
Docker Terminology
Learn terms and definitions for Docker technology.
Docker containers, images, and registries
Find out how Docker container images are stored in an image registry for consistent deployment across
environments.
Build Docker Images for .NET Core Applications
Learn how to build and dockerize an ASP.NET Core app. Explore Docker images maintained by Microsoft and
examine use cases.
Visual Studio Tools for Docker
Discover how Visual Studio 2017 supports building, debugging, and running ASP.NET Core apps targeting either
.NET Framework or .NET Core on Docker for Windows. Both Windows and Linux containers are supported.
Publish to a Docker Image
Find out how to use the Visual Studio Tools for Docker extension to deploy an ASP.NET Core app to a Docker host
on Azure using PowerShell.
Configure ASP.NET Core to work with proxy servers and load balancers
Additional configuration might be required for apps hosted behind proxy servers and load balancers. Passing
requests through a proxy often obscures information about the original request, such as the scheme and client IP.
It might be necessary to forwarded some information about the request manually to the app.
Visual Studio Tools for Docker with ASP.NET Core
9/13/2018 • 9 minutes to read • Edit Online
Visual Studio 2017 supports building, debugging, and running containerized ASP.NET Core apps targeting .NET
Core. Both Windows and Linux containers are supported.
View or download sample code (how to download)
Prerequisites
Docker for Windows
Visual Studio 2017 with the .NET Core cross-platform development workload
TIP
Visual Studio 2017 versions 15.6 and later prompt when Shared Drives aren't configured.
If the target framework is .NET Core, the OS drop-down allows for the selection of a container type.
Existing app
For ASP.NET Core projects targeting .NET Core, there are two options for adding Docker support via the tooling.
Open the project in Visual Studio, and choose one of the following options:
Select Docker Support from the Project menu.
Right-click the project in Solution Explorer and select Add > Docker Support.
The Visual Studio Tools for Docker don't support adding Docker to an existing ASP.NET Core project targeting
.NET Framework.
Dockerfile overview
A Dockerfile, the recipe for creating a final Docker image, is added to the project root. Refer to Dockerfile reference
for an understanding of the commands within it. This particular Dockerfile uses a multi-stage build with four
distinct, named build stages:
The preceding Dockerfile is based on the microsoft/dotnet image. This base image includes the ASP.NET Core
runtime and NuGet packages. The packages are just-in-time (JIT) compiled to improve startup performance.
When the new project dialog's Configure for HTTPS check box is checked, the Dockerfile exposes two ports. One
port is used for HTTP traffic; the other port is used for HTTPS. If the check box isn't checked, a single port (80) is
exposed for HTTP traffic.
The preceding Dockerfile is based on the microsoft/aspnetcore image. This base image includes the ASP.NET Core
NuGet packages, which are just-in-time (JIT) compiled to improve startup performance.
version: '3.4'
services:
hellodockertools:
image: ${DOCKER_REGISTRY}hellodockertools
build:
context: .
dockerfile: HelloDockerTools/Dockerfile
In the preceding example, image: hellodockertools generates the image hellodockertools:dev when the app runs
in Debug mode. The hellodockertools:latest image is generated when the app runs in Release mode.
Prefix the image name with the Docker Hub username (for example, dockerhubusername/hellodockertools ) if the
image is pushed to the registry. Alternatively, change the image name to include the private registry URL (for
example, privateregistry.domain.com/hellodockertools ) depending on the configuration.
If you want different behavior based on the build configuration (for example, Debug or Release), add
configuration-specific docker-compose files. The files should be named according to the build configuration (for
example, docker-compose.vs.debug.yml and docker-compose.vs.release.yml) and placed in the same location as the
docker-compose-override.yml file.
Using the configuration-specific override files, you can specify different configuration settings (such as
environment variables or entry points) for Debug and Release build configurations.
Service Fabric
In addition to the base Prerequisites, the Service Fabric orchestration solution demands the following
prerequisites:
Microsoft Azure Service Fabric SDK version 2.6 or later
Visual Studio 2017's Azure Development workload
Service Fabric doesn't support running Linux containers in the local development cluster on Windows. If the
project is already using a Linux container, Visual Studio prompts to switch to Windows containers.
The Visual Studio Tools for Docker do the following tasks:
Adds a <project_name>Application Service Fabric Application project to the solution.
Adds a Dockerfile and a .dockerignore file to the ASP.NET Core project. If a Dockerfile already exists in the
ASP.NET Core project, it's renamed to Dockerfile.original. A new Dockerfile, similar to the following, is
created:
# See https://aka.ms/containerimagehelp for information on how to use Windows Server 1709 containers
with Service Fabric.
# FROM microsoft/aspnetcore:2.0-nanoserver-1709
FROM microsoft/aspnetcore:2.0-nanoserver-sac2016
ARG source
WORKDIR /app
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "HelloDockerTools.dll"]
<IsServiceFabricServiceProject>True</IsServiceFabricServiceProject>
Adds a PackageRoot folder to the ASP.NET Core project. The folder includes the service manifest and
settings for the new service.
For more information, see Deploy a .NET app in a Windows container to Azure Service Fabric.
Debug
Select Docker from the debug drop-down in the toolbar, and start debugging the app. The Docker view of the
Output window shows the following actions taking place:
The 2.1 -aspnetcore-runtime tag of the microsoft/dotnet runtime image is acquired (if not already in the cache).
The image installs the ASP.NET Core and .NET Core runtimes and associated libraries. It's optimized for
running ASP.NET Core apps in production.
The ASPNETCORE_ENVIRONMENT environment variable is set to Development within the container.
Two dynamically assigned ports are exposed: one for HTTP and one for HTTPS. The port assigned to localhost
can be queried with the docker ps command.
The app is copied to the container.
The default browser is launched with the debugger attached to the container using the dynamically assigned
port.
The resulting Docker image of the app is tagged as dev. The image is based on the 2.1 -aspnetcore-runtime tag of
the microsoft/dotnet base image. Run the docker images command in the Package Manager Console (PMC )
window. The images on the machine are displayed:
The microsoft/aspnetcore runtime image is acquired (if not already in the cache).
The ASPNETCORE_ENVIRONMENT environment variable is set to Development within the container.
Port 80 is exposed and mapped to a dynamically assigned port for localhost. The port is determined by the
Docker host and can be queried with the docker ps command.
The app is copied to the container.
The default browser is launched with the debugger attached to the container using the dynamically assigned
port.
The resulting Docker image of the app is tagged as dev. The image is based on the microsoft/aspnetcore base
image. Run the docker images command in the Package Manager Console (PMC ) window. The images on the
machine are displayed:
NOTE
The dev image lacks the app contents, as Debug configurations use volume mounting to provide the iterative experience. To
push an image, use the Release configuration.
Run the docker ps command in PMC. Notice the app is running using the container:
The microsoft/aspnetcore-build and microsoft/aspnetcore images listed in the preceding output are replaced with
microsoft/dotnet images as of .NET Core 2.1. For more information, see the Docker repositories migration
announcement.
NOTE
The docker images command returns intermediary images with repository names and tags identified as <none> (not
listed above). These unnamed images are produced by the multi-stage build Dockerfile. They improve the efficiency of
building the final image—only the necessary layers are rebuilt when changes occur. When the intermediary images are no
longer needed, delete them using the docker rmi command.
There may be an expectation for the production or release image to be smaller in size by comparison to the dev
image. Because of the volume mapping, the debugger and app were running from the local machine and not
within the container. The latest image has packaged the necessary app code to run the app on a host machine.
Therefore, the delta is the size of the app code.
Additional resources
Container development with Visual Studio
Azure Service Fabric: Prepare your development environment
Deploy a .NET app in a Windows container to Azure Service Fabric
Troubleshoot Visual Studio 2017 development with Docker
Visual Studio Tools for Docker GitHub repository
Configure ASP.NET Core to work with proxy servers
and load balancers
9/21/2018 • 13 minutes to read • Edit Online
Forwarded headers
By convention, proxies forward information in HTTP headers.
HEADER DESCRIPTION
X-Forwarded-For Holds information about the client that initiated the request
and subsequent proxies in a chain of proxies. This parameter
may contain IP addresses (and, optionally, port numbers). In
a chain of proxy servers, the first parameter indicates the
client where the request was first made. Subsequent proxy
identifiers follow. The last proxy in the chain isn't in the list of
parameters. The last proxy's IP address, and optionally a
port number, are available as the remote IP address at the
transport layer.
X-Forwarded-Host The original value of the Host header field. Usually, proxies
don't modify the Host header. See Microsoft Security
Advisory CVE-2018-0787 for information on an elevation-
of-privileges vulnerability that affects systems where the
proxy doesn't validate or restict Host headers to known
good values.
The Forwarded Headers Middleware, from the Microsoft.AspNetCore.HttpOverrides package, reads these
headers and fills in the associated fields on HttpContext.
The middleware updates:
HttpContext.Connection.RemoteIpAddress – Set using the X-Forwarded-For header value. Additional
settings influence how the middleware sets RemoteIpAddress . For details, see the Forwarded Headers
Middleware options.
HttpContext.Request.Scheme – Set using the X-Forwarded-Proto header value.
HttpContext.Request.Host – Set using the X-Forwarded-Host header value.
Forwarded Headers Middleware default settings can be configured. The default settings are:
There is only one proxy between the app and the source of the requests.
Only loopback addresses are configured for known proxies and known networks.
The forwarded headers are named X-Forwarded-For and X-Forwarded-Proto .
Not all network appliances add the X-Forwarded-For and X-Forwarded-Proto headers without additional
configuration. Consult your appliance manufacturer's guidance if proxied requests don't contain these headers
when they reach the app. If the appliance uses different header names than X-Forwarded-For and
X-Forwarded-Proto , set the ForwardedForHeaderName and ForwardedProtoHeaderName options to match
the header names used by the appliance. For more information, see Forwarded Headers Middleware options
and Configuration for a proxy that uses different header names.
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
// In ASP.NET Core 1.x, replace the following line with: app.UseIdentity();
app.UseAuthentication();
app.UseMvc();
}
NOTE
If no ForwardedHeadersOptions are specified in Startup.ConfigureServices or directly to the extension method with
UseForwardedHeaders(IApplicationBuilder, ForwardedHeadersOptions), the default headers to forward are
ForwardedHeaders.None. The ForwardedHeadersOptions.ForwardedHeaders property must be configured with the
headers to forward.
Nginx configuration
To forward the X-Forwarded-For and X-Forwarded-Proto headers, see Host ASP.NET Core on Linux with Nginx.
For more information, see NGINX: Using the Forwarded header.
Apache configuration
X-Forwarded-For is added automatically (see Apache Module mod_proxy: Reverse Proxy Request Headers). For
information on how to forward the X-Forwarded-Proto header, see Host ASP.NET Core on Linux with Apache.
OPTION DESCRIPTION
ForwardedForHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XForwardedForHeaderName.
This option is used when the proxy/forwarder doesn't use
the X-Forwarded-For header but uses some other header
to forward the information.
ForwardedHostHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XForwardedHostHeaderName.
This option is used when the proxy/forwarder doesn't use
the X-Forwarded-Host header but uses some other
header to forward the information.
ForwardedProtoHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XForwardedProtoHeaderName.
This option is used when the proxy/forwarder doesn't use
the X-Forwarded-Proto header but uses some other
header to forward the information.
The default is 1.
OriginalForHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XOriginalForHeaderName.
OriginalHostHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XOriginalHostHeaderName.
OriginalProtoHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XOriginalProtoHeaderName.
OPTION DESCRIPTION
ForwardedForHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XForwardedForHeaderName.
This option is used when the proxy/forwarder doesn't use
the X-Forwarded-For header but uses some other header
to forward the information.
ForwardedHostHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XForwardedHostHeaderName.
This option is used when the proxy/forwarder doesn't use
the X-Forwarded-Host header but uses some other
header to forward the information.
ForwardedProtoHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XForwardedProtoHeaderName.
This option is used when the proxy/forwarder doesn't use
the X-Forwarded-Proto header but uses some other
header to forward the information.
The default is 1.
OriginalForHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XOriginalForHeaderName.
OriginalHostHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XOriginalHostHeaderName.
OriginalProtoHeaderName Use the header specified by this property instead of the one
specified by
ForwardedHeadersDefaults.XOriginalProtoHeaderName.
This code can be disabled with an environment variable or other configuration setting in a development or
staging environment.
Deal with path base and proxies that change the request path
Some proxies pass the path intact but with an app base path that should be removed so that routing works
properly. UsePathBaseExtensions.UsePathBase middleware splits the path into HttpRequest.Path and the app
base path into HttpRequest.PathBase.
If /foo is the app base path for a proxy path passed as /foo/api/1 , the middleware sets Request.PathBase to
/foo and Request.Path to /api/1 with the following command:
app.UsePathBase("/foo");
The original path and path base are reapplied when the middleware is called again in reverse. For more
information on middleware order processing, see ASP.NET Core Middleware.
If the proxy trims the path (for example, forwarding /foo/api/1 to /api/1 ), fix redirects and links by setting
the request's PathBase property:
If the proxy is adding path data, discard part of the path to fix redirects and links by using
StartsWithSegments(PathString, PathString) and assigning to the Path property:
app.Use((context, next) =>
{
if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
{
context.Request.Path = remainder;
}
return next();
});
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedForHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-For_Header";
options.ForwardedProtoHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-Proto_Header";
});
Troubleshoot
When headers aren't forwarded as expected, enable logging. If the logs don't provide sufficient information to
troubleshoot the problem, enumerate the request headers received by the server. Use inline middleware to
write request headers to an app response or log the headers. Place either of the following code examples
immediately after the call to UseForwardedHeaders in Startup.Configure .
To write the headers to the app's response, use the following terminal inline middleware:
// Headers
await context.Response.WriteAsync($"Request Headers:{Environment.NewLine}");
await context.Response.WriteAsync(Environment.NewLine);
// Connection: RemoteIp
await context.Response.WriteAsync(
$"Request RemoteIp: {context.Connection.RemoteIpAddress}");
});
You can also write to logs instead of the response body by using the following inline middleware. This allows
the site to function normally while debugging.
// Headers
foreach (var header in context.Request.Headers)
{
logger.LogDebug("Header: {KEY}: {VALUE}", header.Key, header.Value);
}
// Connection: RemoteIp
logger.LogDebug("Request RemoteIp: {REMOTE_IP_ADDRESS}",
context.Connection.RemoteIpAddress);
await next();
});
In the preceding example, 10.0.0.100 is a proxy server. If the server is a trusted proxy, add the server's IP
address to KnownProxies (or add a trusted network to KnownNetworks ) in Startup.ConfigureServices . For more
information, see the Forwarded Headers Middleware options section.
services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});
IMPORTANT
Only allow trusted proxies and networks to forward headers. Otherwise, IP spoofing attacks are possible.
Additional resources
Host ASP.NET Core in a web farm
Microsoft Security Advisory CVE -2018-0787: ASP.NET Core Elevation Of Privilege Vulnerability
Host ASP.NET Core in a web farm
7/16/2018 • 4 minutes to read • Edit Online
General configuration
Host and deploy ASP.NET Core
Learn how to set up hosting environments and deploy ASP.NET Core apps. Configure a process manager on
each node of the web farm to automate app starts and restarts. Each node requires the ASP.NET Core runtime.
For more information, see the topics in the Host and deploy area of the documentation.
Configure ASP.NET Core to work with proxy servers and load balancers
Learn about configuration for apps hosted behind proxy servers and load balancers, which often obscure
important request information.
Host ASP.NET Core on Azure App Service
Azure App Service is a Microsoft cloud computing platform service for hosting web apps, including ASP.NET
Core. App Service is a fully managed platform that provides automatic scaling, load balancing, patching, and
continuous deployment.
App data
When an app is scaled to multiple instances, there might be app state that requires sharing across nodes. If the
state is transient, consider sharing an IDistributedCache. If the shared state requires persistence, consider storing
the shared state in a database.
Required configuration
Data Protection and Caching require configuration for apps deployed to a web farm.
Data Protection
The ASP.NET Core Data Protection system is used by apps to protect data. Data Protection relies upon a set of
cryptographic keys stored in a key ring. When the Data Protection system is initialized, it applies default settings
that store the key ring locally. Under the default configuration, a unique key ring is stored on each node of the
web farm. Consequently, each web farm node can't decrypt data that's encrypted by an app on any other node.
The default configuration isn't generally appropriate for hosting apps in a web farm. An alternative to
implementing a shared key ring is to always route user requests to the same node. For more information on
Data Protection system configuration for web farm deployments, see Configure ASP.NET Core Data Protection.
Caching
In a web farm environment, the caching mechanism must share cached items across the web farm's nodes.
Caching must either rely upon a common Redis cache, a shared SQL Server database, or a custom caching
implementation that shares cached items across the web farm. For more information, see Work with a
distributed cache in ASP.NET Core.
Dependent components
The following scenarios don't require additional configuration, but they depend on technologies that require
configuration for web farms.
SCENARIO DEPENDS ON …
Troubleshoot
When Data Protection or Caching isn't configured for a web farm environment, intermittent errors occur when
requests are processed. This occurs because nodes don't share the same resources and user requests aren't
always routed back to the same node.
Consider a user who signs into the app using cookie authentication. The user signs into the app on one web farm
node. If their next request arrives at the same node where they signed in, the app is able to decrypt the
authentication cookie and allows access to the app's resource. If their next request arrives at a different node, the
app can't decrypt the authentication cookie from the node where the user signed in, and authorization for the
requested resource fails.
When any of the following symptoms occur intermittently, the problem is usually traced to improper Data
Protection or Caching configuration for a web farm environment:
Authentication breaks – The authentication cookie is misconfigured or can't be decrypted. OAuth (Facebook,
Microsoft, Twitter) or OpenIdConnect logins fail with the error "Correlation failed."
Authorization breaks – Identity is lost.
Session state loses data.
Cached items disappear.
TempData fails.
POSTs fail – The anti-forgery check fails.
For more information on Data Protection configuration for web farm deployments, see Configure ASP.NET Core
Data Protection. For more information on Caching configuration for web farm deployments, see Work with a
distributed cache in ASP.NET Core.
Visual Studio publish profiles for ASP.NET Core app
deployment
9/13/2018 • 11 minutes to read • Edit Online
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
</Project>
C:\Webs\Web1>dotnet publish
Microsoft (R) Build Engine version 15.3.409.57025 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
The dotnet publish command calls MSBuild, which invokes the Publish target. Any parameters passed to
dotnet publish are passed to MSBuild. The -c parameter maps to the Configuration MSBuild property. The
-o parameter maps to OutputPath .
The network share is specified with forward slashes ( //r8/) and works on all .NET Core supported platforms.
Confirm that the published app for deployment isn't running. Files in the publish folder are locked when the app is
running. Deployment can't occur because locked files can't be copied.
Publish profiles
This section uses Visual Studio 2017 to create a publishing profile. Once created, publishing from Visual Studio or
the command line is available.
Publish profiles can simplify the publishing process, and any number of profiles can exist. Create a publish profile
in Visual Studio by choosing one of the following paths:
Right-click the project in Solution Explorer and select Publish.
Select Publish <project_name> from the Build menu.
The Publish tab of the app capacities page is displayed. If the project lacks a publish profile, the following page is
displayed:
When Folder is selected, specify a folder path to store the published assets. The default folder is
bin\Release\PublishOutput. Click the Create Profile button to finish.
Once a publish profile is created, the Publish tab changes. The newly created profile appears in a drop-down list.
Click Create new profile to create another new profile.
The Publish wizard supports the following publish targets:
Azure App Service
Azure Virtual Machines
IIS, FTP, etc. (for any web server)
Folder
Import Profile
For more information, see What publishing options are right for me.
When creating a publish profile with Visual Studio, a Properties/PublishProfiles/<profile_name>.pubxml MSBuild
file is created. The .pubxml file is a MSBuild file and contains publish configuration settings. This file can be
changed to customize the build and publish process. This file is read by the publishing process.
<LastUsedBuildConfiguration> is special because it's a global property and shouldn't be in any file that's imported
in the build. See MSBuild: how to set the configuration property for more information.
When publishing to an Azure target, the .pubxml file contains your Azure subscription identifier. With that target
type, adding this file to source control is discouraged. When publishing to a non-Azure target, it's safe to check in
the .pubxml file.
Sensitive information (like the publish password) is encrypted on a per user/machine level. It's stored in the
Properties/PublishProfiles/<profile_name>.pubxml.user file. Because this file can store sensitive information, it
shouldn't be checked into source control.
For an overview of how to publish a web app on ASP.NET Core, see Host and deploy. The MSBuild tasks and
targets necessary to publish an ASP.NET Core app are open-source at https://github.com/aspnet/websdk.
dotnet publish can use folder, MSDeploy, and Kudu publish profiles:
Folder (works cross-platform):
dotnet publish WebApplication.csproj /p:PublishProfile=<FolderProfileName>
MSDeploy (currently this only works in Windows since MSDeploy isn't cross-platform):
MSDeploy package (currently this only works in Windows since MSDeploy isn't cross-platform):
<Project>
<PropertyGroup>
<PublishProtocol>Kudu</PublishProtocol>
<PublishSiteName>nodewebapp</PublishSiteName>
<UserName>username</UserName>
<Password>password</Password>
</PropertyGroup>
</Project>
Run the following command to zip up the publish contents and publish it to Azure using the Kudu APIs:
When publishing with a profile named FolderProfile, either of the commands below can be executed:
dotnet build /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
msbuild /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
When invoking dotnet build, it calls msbuild to run the build and publish process. Calling either dotnet build or
msbuild is equivalent when passing in a folder profile. When calling MSBuild directly on Windows, the .NET
Framework version of MSBuild is used. MSDeploy is currently limited to Windows machines for publishing.
Calling dotnet build on a non-folder profile invokes MSBuild, and MSBuild uses MSDeploy on non-folder
profiles. Calling dotnet build on a non-folder profile invokes MSBuild (using MSDeploy) and results in a failure
(even when running on a Windows platform). To publish with a non-folder profile, call MSBuild directly.
The following folder publish profile was created with Visual Studio and publishes to a network share:
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project.
You can customize the behavior of this process by editing this
MSBuild file.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<PublishFramework>netcoreapp1.1</PublishFramework>
<ProjectGuid>c30c453c-312e-40c4-aec9-394a145dee0b</ProjectGuid>
<publishUrl>\\r8\Release\AdminWeb</publishUrl>
<DeleteExistingFiles>False</DeleteExistingFiles>
</PropertyGroup>
</Project>
Note <LastUsedBuildConfiguration> is set to Release . When publishing from Visual Studio, the
<LastUsedBuildConfiguration> configuration property value is set using the value when the publish process is
started. The <LastUsedBuildConfiguration> configuration property is special and shouldn't be overridden in an
imported MSBuild file. This property can be overridden from the command line.
Using the .NET Core CLI:
Using MSBuild:
Get the Password from the <Publish name>.PublishSettings file. Download the .PublishSettings file from either:
Solution Explorer: Right-click on the Web App and select Download Publish Profile.
Azure portal: Click Get publish profile on the Web App's Overview panel.
Username can be found in the publish profile.
The following sample uses the Web11112 - Web Deploy publish profile:
Exclude files
When publishing ASP.NET Core web apps, the build artifacts and contents of the wwwroot folder are included.
msbuild supports globbing patterns. For example, the following <Content> element excludes all text (.txt) files
from the wwwroot/content folder and all its subfolders.
<ItemGroup>
<Content Update="wwwroot/content/**/*.txt" CopyToPublishDirectory="Never" />
</ItemGroup>
The preceding markup can be added to a publish profile or the .csproj file. When added to the .csproj file, the rule
is added to all publish profiles in the project.
The following <MsDeploySkipRules> element excludes all files from the wwwroot/content folder:
<ItemGroup>
<MsDeploySkipRules Include="CustomSkipFolder">
<ObjectName>dirPath</ObjectName>
<AbsolutePath>wwwroot\\content</AbsolutePath>
</MsDeploySkipRules>
</ItemGroup>
<MsDeploySkipRules> won't delete the skip targets from the deployment site. <Content> targeted files and folders
are deleted from the deployment site. For example, suppose a deployed web app had the following files:
Views/Home/About1.cshtml
Views/Home/About2.cshtml
Views/Home/About3.cshtml
If the following <MsDeploySkipRules> elements are added, those files wouldn't be deleted on the deployment site.
<ItemGroup>
<MsDeploySkipRules Include="CustomSkipFile">
<ObjectName>filePath</ObjectName>
<AbsolutePath>Views\\Home\\About1.cshtml</AbsolutePath>
</MsDeploySkipRules>
<MsDeploySkipRules Include="CustomSkipFile">
<ObjectName>filePath</ObjectName>
<AbsolutePath>Views\\Home\\About2.cshtml</AbsolutePath>
</MsDeploySkipRules>
<MsDeploySkipRules Include="CustomSkipFile">
<ObjectName>filePath</ObjectName>
<AbsolutePath>Views\\Home\\About3.cshtml</AbsolutePath>
</MsDeploySkipRules>
</ItemGroup>
The preceding <MsDeploySkipRules> elements prevent the skipped files from being deployed. It won't delete those
files once they're deployed.
The following <Content> element deletes the targeted files at the deployment site:
<ItemGroup>
<Content Update="Views/Home/About?.cshtml" CopyToPublishDirectory="Never" />
</ItemGroup>
Using command-line deployment with the preceding <Content> element yields the following output:
MSDeployPublish:
Starting Web deployment task from source:
manifest(C:\Webs\Web1\obj\Release\netcoreapp1.1\PubTmp\Web1.SourceManifest.
xml) to Destination: auto().
Deleting file (Web11112\Views\Home\About1.cshtml).
Deleting file (Web11112\Views\Home\About2.cshtml).
Deleting file (Web11112\Views\Home\About3.cshtml).
Updating file (Web11112\web.config).
Updating file (Web11112\Web1.deps.json).
Updating file (Web11112\Web1.dll).
Updating file (Web11112\Web1.pdb).
Updating file (Web11112\Web1.runtimeconfig.json).
Successfully executed Web deployment task.
Publish Succeeded.
Done Building Project "C:\Webs\Web1\Web1.csproj" (default targets).
Include files
The following markup includes an images folder outside the project directory to the wwwroot/images folder of
the publish site:
<ItemGroup>
<_CustomFiles Include="$(MSBuildProjectDirectory)/../images/**/*" />
<DotnetPublishFiles Include="@(_CustomFiles)">
<DestinationRelativePath>wwwroot/images/%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</DotnetPublishFiles>
</ItemGroup>
The markup can be added to the .csproj file or the publish profile. If it's added to the .csproj file, it's included in
each publish profile in the project.
The following highlighted markup shows how to:
Copy a file from outside the project into the wwwroot folder.
Exclude the wwwroot\Content folder.
Exclude Views\Home\About2.cshtml.
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project.
You can customize the behavior of this process by editing this
MSBuild file.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data>
<PublishFramework />
<ProjectGuid>afa9f185-7ce0-4935-9da1-ab676229d68a</ProjectGuid>
<publishUrl>bin\Release\PublishOutput</publishUrl>
<DeleteExistingFiles>False</DeleteExistingFiles>
</PropertyGroup>
<ItemGroup>
<ResolvedFileToPublish Include="..\ReadMe2.MD">
<RelativePath>wwwroot\ReadMe2.MD</RelativePath>
</ResolvedFileToPublish>
</ItemGroup>
</Project>
<PropertyGroup>
<AllowUntrustedCertificate>True</AllowUntrustedCertificate>
</PropertyGroup>
Select the Debug Console menu item to view, edit, delete, or add files.
Additional resources
Web Deploy (MSDeploy) simplifies deployment of web apps and websites to IIS servers.
https://github.com/aspnet/websdk: File issues and request features for deployment.
Publish an ASP.NET Web App to an Azure VM from Visual Studio
ASP.NET Core directory structure
6/26/2018 • 2 minutes to read • Edit Online
By Luke Latham
In ASP.NET Core, the published application directory, publish, is comprised of application files, config files, static
assets, packages, and the runtime (for self-contained deployments).
†Indicates a directory
The publish directory represents the content root path, also called the application base path, of the deployment.
Whatever name is given to the publish directory of the deployed app on the server, its location serves as the
server's physical path to the hosted app.
The wwwroot directory, if present, only contains static assets.
The stdout Logs directory can be created for the deployment using one of the following two approaches:
Add the following <Target> element to the project file:
The <MakeDir> element creates an empty Logs folder in the published output. The element uses the
PublishDir property to determine the target location for creating the folder. Several deployment
methods, such as Web Deploy, skip empty folders during deployment. The <WriteLinesToFile> element
generates a file in the Logs folder, which guarantees deployment of the folder to the server. Note that
folder creation may still fail if the worker process doesn't have write access to the target folder.
Physically create the Logs directory on the server in the deployment.
The deployment directory requires Read/Execute permissions. The Logs directory requires Read/Write
permissions. Additional directories where files are written require Read/Write permissions.
Common errors reference for Azure App Service
and IIS with ASP.NET Core
6/21/2018 • 8 minutes to read • Edit Online
By Luke Latham
The following isn't a complete list of errors. If you encounter an error not listed here, open a new issue with
detailed instructions to reproduce the error.
Collect the following information:
Browser behavior
Application Event Log entries
ASP.NET Core Module stdout log entries
Compare the information to the following common errors. If a match is found, follow the troubleshooting advice.
IMPORTANT
ASP.NET Core preview releases with Azure App Service
ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core
preview release, see Deploy ASP.NET Core preview release to Azure App Service.
Troubleshooting:
If the system doesn't have Internet access while installing the Hosting Bundle, this exception occurs when the
installer is prevented from obtaining the Microsoft Visual C++ 2015 Redistributable. Obtain an installer from
the Microsoft Download Center. If the installer fails, the server may not receive the .NET Core runtime
required to host a framework-dependent deployment (FDD ). If hosting an FDD, confirm that the runtime is
installed in Programs & Features. If needed, obtain a runtime installer from .NET All Downloads. After
installing the runtime, restart the system or restart IIS by executing net stop was /y followed by net start
w3svc from a command prompt.
ASP.NET Core enables developers to easily configure and manage security for their apps. ASP.NET Core contains
features for managing authentication, authorization, data protection, SSL enforcement, app secrets, anti-request
forgery protection, and CORS management. These security features allow you to build robust yet secure ASP.NET
Core apps.
By Rick Anderson
ASP.NET Core Identity is a membership system that adds login functionality to ASP.NET Core apps. Users
can create an account with the login information stored in Identity or they can use an external login provider.
Supported external login providers include Facebook, Google, Microsoft Account, and Twitter.
Identity can be configured using a SQL Server database to store user names, passwords, and profile data.
Alternatively, another persistent store can be used, for example, Azure Table Storage.
View or download the sample code. (How to download)
In this topic, you learn how to use Identity to register, log in, and log out a user. For more detailed instructions
about creating apps that use Identity, see the Next Steps section at the end of this article.
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = false;
});
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.SlidingExpiration = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
The preceding code configures Identity with default option values. Services are made available to the app
through dependency injection.
Identity is enabled by calling UseAuthentication. UseAuthentication adds authentication middleware to the
request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
// If the LoginPath isn't set, ASP.NET Core defaults
// the path to /Account/Login.
options.LoginPath = "/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults
// the path to /Account/AccessDenied.
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
});
services.AddMvc();
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
// Configure Identity
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// Cookie settings
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
// User settings
options.User.RequireUniqueEmail = true;
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
For more information, see the IdentityOptions Class and Application Startup.
When a user clicks the Register link, the Register action is invoked on AccountController . The Register
action creates the user by calling CreateAsync on the _userManager object (provided to AccountController by
dependency injection):
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// For more information on how to enable account confirmation and password reset please visit
http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
//var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
//var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code
}, protocol: HttpContext.Request.Scheme);
//await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
// "Please confirm your account by clicking this link: <a href=\"" + callbackUrl +
"\">link</a>");
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(3, "User created a new account with password.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
AddErrors(result);
}
If the user was created successfully, the user is logged in by the call to _signInManager.SignInAsync .
Note: See account confirmation for steps to prevent immediate login at registration.
Log in
The Login form is displayed when:
The Log in link is selected.
When a user accesses a page where they are not authenticated or authorized, they are redirected to the
Login page.
When the form on the Login page is submitted, the OnPostAsync action is called. PasswordSignInAsync is called
on the _signInManager object (provided by dependency injection).
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout,
// set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email,
Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl,
RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
The base Controller class exposes a User property that you can access from controller methods. For
instance, you can enumerate User.Claims and make authorization decisions. For more information, see
Authorization.
The Login form is displayed when users select the Log in link or are redirected when accessing a page that
requires authentication. When the user submits the form on the Login page, the AccountController Login
action is called.
The Login action calls PasswordSignInAsync on the _signInManager object (provided to AccountController by
dependency injection).
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email,
model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe =
model.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning(2, "User account locked out.");
return View("Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
The base ( Controller or PageModel ) class exposes a User property. For example, User.Claims can be
enumerated to make authorization decisions.
Log out
The Log out link invokes the LogoutModel.OnPost action.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace WebApp1.Areas.Identity.Pages.Account
{
[AllowAnonymous]
public class LogoutModel : PageModel
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;
SignOutAsync clears the user's claims stored in a cookie. Don't redirect after calling SignOutAsync or the user
will not be signed out.
Post is specified in the Pages/Shared/_LoginPartial.cshtml:
@using Microsoft.AspNetCore.Identity
@if (SignInManager.IsSignedIn(User))
{
<form asp-area="Identity" asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Page("/Index", new { area = "" })"
method="post"
id="logoutForm" class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">
Hello @UserManager.GetUserName(User)!</a>
</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Logout</button>
</li>
</ul>
</form>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="Identity" asp-page="/Account/Register">Register</a></li>
<li><a asp-area="Identity" asp-page="/Account/Login">Login</a></li>
</ul>
}
//
// POST: /Account/LogOut
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LogOut()
{
await _signInManager.SignOutAsync();
_logger.LogInformation(4, "User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
The preceding code calls the _signInManager.SignOutAsync method. The SignOutAsync method clears the user's
claims stored in a cookie.
Test Identity
The default web project templates allow anonymous access to the home pages. To test Identity, add
[Authorize] to the About page.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebApp1.Pages
{
[Authorize]
public class AboutModel : PageModel
{
public string Message { get; set; }
If you are signed in, sign out. Run the app and select the About link. You are redirected to the login page.
Explore Identity
To explore Identity in more detail:
Create full identity UI source
Examine the source of each page and step through the debugger.
Identity Components
All the Identity dependent NuGet packages are included in the Microsoft.AspNetCore.App metapackage.
The primary package for Identity is Microsoft.AspNetCore.Identity. This package contains the core set of
interfaces for ASP.NET Core Identity, and is included by Microsoft.AspNetCore.Identity.EntityFrameworkCore .
Next Steps
Configure Identity
Create an ASP.NET Core app with user data protected by authorization
Add, download, and delete custom user data to Identity in an ASP.NET Core project
Enable QR Code generation for TOTP authenticator apps in ASP.NET Core
Configure Identity primary keys data type.
Migrate Authentication and Identity to ASP.NET Core
Account confirmation and password recovery in ASP.NET Core
Two-factor authentication with SMS in ASP.NET Core
Host ASP.NET Core in a web farm
Scaffold Identity in ASP.NET Core projects
9/21/2018 • 12 minutes to read • Edit Online
By Rick Anderson
ASP.NET Core 2.1 and later provides ASP.NET Core Identity as a Razor Class Library. Applications that include
Identity can apply the scaffolder to selectively add the source code contained in the Identity Razor Class Library
(RCL ). You might want to generate source code so you can modify the code and change the behavior. For
example, you could instruct the scaffolder to generate the code used in registration. Generated code takes
precedence over the same code in the Identity RCL. To gain full control of the UI and not use the default RCL, see
the section Create full identity UI source.
Applications that do not include authentication can apply the scaffolder to add the RCL Identity package. You
have the option of selecting Identity code to be generated.
Although the scaffolder generates most of the necessary code, you'll have to update your project to complete the
process. This document explains the steps needed to complete an Identity scaffolding update.
When the Identity scaffolder is run, a ScaffoldingReadme.txt file is created in the project directory. The
ScaffoldingReadme.txt file contains general instructions on what's needed to complete the Identity scaffolding
update. This document contains more complete instructions than the ScaffoldingReadme.txt file.
We recommend using a source control system that shows file differences and allows you to back out of changes.
Inspect the changes after running the Identity scaffolder.
NOTE
Services are required when using Two Factor Authentication, Account confirmation and password recovery, and other
security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these
features must be added manually. For example, see Require Email Confirmation.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc();
}
}
UseHsts is recommended but not required. See HTTP Strict Transport Security Protocol for more information.
The generated Identity database code requires Entity Framework Core Migrations. Create a migration and update
the database. For example, run the following commands:
Visual Studio
.NET Core CLI
In the Visual Studio Package Manager Console:
Add-Migration CreateIdentitySchema
Update-Database
Add-Migration CreateIdentitySchema
Update-Database
Enable authentication
In the Configure method of the Startup class, call UseAuthentication after UseStaticFiles :
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc();
}
}
UseHsts is recommended but not required. See HTTP Strict Transport Security Protocol for more information.
Layout changes
Optional: Add the login partial ( _LoginPartial ) to the layout file:
<!DOCTYPE html>
<html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorNoAuth8</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">RazorNoAuth8</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Contact">Contact</a></li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
<!DOCTYPE html>
<html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - MvcNoAuth3</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-
value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-
collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">MvcNoAuth3</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
<partial name="_LoginPartial" />
</div>
</div>
</nav>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
Add-Migration CreateIdentitySchema
Update-Database
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
}
UseHsts is recommended but not required. See HTTP Strict Transport Security Protocol for more information.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
// services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
});
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
// using Microsoft.AspNetCore.Identity.UI.Services;
services.AddSingleton<IEmailSender, EmailSender>();
}
services.AddIdentity<IdentityUser, IdentityRole>()
// services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
The following the code sets the LoginPath, LogoutPath, and AccessDeniedPath:
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
// using Microsoft.AspNetCore.Identity.UI.Services;
services.AddSingleton<IEmailSender, EmailSender>();
public class EmailSender : IEmailSender
{
public Task SendEmailAsync(string email, string subject, string message)
{
return Task.CompletedTask;
}
}
Additional resources
Changes to authentication code to ASP.NET Core 2.1 and later
Add, download, and delete custom user data to
Identity in an ASP.NET Core project
9/21/2018 • 7 minutes to read • Edit Online
By Rick Anderson
This article shows how to:
Add custom user data to an ASP.NET Core web app.
Decorate the custom user data model with the PersonalData attribute so it's automatically available for
download and deletion. Making the data able to be downloaded and deleted helps meet GDPR requirements.
The project sample is created from a Razor Pages web app, but the instructions are similar for a ASP.NET Core
MVC web app.
View or download sample code (how to download)
Prerequisites
.NET Core 2.1 SDK or later
using Microsoft.AspNetCore.Identity;
using System;
namespace WebApp1.Areas.Identity.Data
{
public class WebApp1User : IdentityUser
{
[PersonalData]
public string Name { get; set; }
[PersonalData]
public DateTime DOB { get; set; }
}
}
public IndexModel(
UserManager<WebApp1User> userManager,
SignInManager<WebApp1User> signInManager,
IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
}
[TempData]
public string StatusMessage { get; set; }
[BindProperty]
public InputModel Input { get; set; }
[Required]
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime DOB { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
Username = userName;
return Page();
}
if (Input.Name != user.Name)
{
user.Name = Input.Name;
}
if (Input.DOB != user.DOB)
{
user.DOB = Input.DOB;
}
await _userManager.UpdateAsync(user);
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<div class="row">
<div class="col-md-6">
<form id="profile-form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Username"></label>
<input asp-for="Username" class="form-control" disabled />
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
@if (Model.IsEmailConfirmed)
{
<div class="input-group">
<input asp-for="Input.Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok
text-success"></span></span>
</div>
}
else
{
<input asp-for="Input.Email" class="form-control" />
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail"
class="btn btn-link">Send verification email</button>
}
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Input.DOB"></label>
<input asp-for="Input.DOB" class="form-control" />
</div>
<label asp-for="Input.PhoneNumber"></label>
<input asp-for="Input.PhoneNumber" class="form-control" />
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Save</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
[BindProperty]
public InputModel Input { get; set; }
[Required]
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime DOB { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",
MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
@page
@model RegisterModel
@{
ViewData["Title"] = "Register";
}
<h2>@ViewData["Title"]</h2>
<div class="row">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h4>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.DOB"></label>
<input asp-for="Input.DOB" class="form-control" />
<span asp-validation-for="Input.DOB" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ConfirmPassword"></label>
<input asp-for="Input.ConfirmPassword" class="form-control" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Add-Migration CustomUserData
Update-Database
Test create, view, download, delete custom user data
Test the app:
Register a new user.
View the custom user data on the /Identity/Account/Manage page.
Download and view the users personal data from the /Identity/Account/Manage/PersonalData page.
Identity model customization in ASP.NET Core
9/27/2018 • 18 minutes to read • Edit Online
By Arthur Vickers
ASP.NET Core Identity provides a framework for managing and storing user accounts in ASP.NET Core apps.
Identity is added to your project when Individual User Accounts is selected as the authentication mechanism. By
default, Identity makes use of an Entity Framework (EF ) Core data model. This article describes how to customize
the Identity model.
builder.Entity<TUser>(b =>
{
// Primary key
b.HasKey(u => u.Id);
// Each User can have many entries in the UserRole join table
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});
builder.Entity<TUserClaim>(b =>
{
// Primary key
b.HasKey(uc => uc.Id);
builder.Entity<TUserLogin>(b =>
{
// Composite primary key consisting of the LoginProvider and the key to use
// with that provider
b.HasKey(l => new { l.LoginProvider, l.ProviderKey });
// Limit the size of the composite key columns due to common DB restrictions
b.Property(l => l.LoginProvider).HasMaxLength(128);
b.Property(l => l.ProviderKey).HasMaxLength(128);
builder.Entity<TUserToken>(b =>
{
// Composite primary key consisting of the UserId, LoginProvider and Name
b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });
// Limit the size of the composite key columns due to common DB restrictions
b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
b.Property(t => t.Name).HasMaxLength(maxKeyLength);
builder.Entity<TRole>(b =>
{
// Primary key
b.HasKey(r => r.Id);
// Each Role can have many entries in the UserRole join table
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
// Each Role can have many associated RoleClaims
b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});
builder.Entity<TRoleClaim>(b =>
{
// Primary key
b.HasKey(rc => rc.Id);
builder.Entity<TUserRole>(b =>
{
// Primary key
b.HasKey(r => new { r.UserId, r.RoleId });
Rather than using these types directly, the types can be used as base classes for the app's own types. The
DbContext classes defined by Identity are generic, such that different CLR types can be used for one or more of the
entity types in the model. These generic types also allow the User primary key (PK) data type to be changed.
When using Identity with support for roles, an IdentityDbContext class should be used. For example:
// Uses all the built-in Identity types
// Uses `string` as the key type
public class IdentityDbContext
: IdentityDbContext<IdentityUser, IdentityRole, string>
{
}
// Uses the built-in Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityDbContext<TUser>
: IdentityDbContext<TUser, IdentityRole, string>
where TUser : IdentityUser
{
}
// Uses the built-in Identity types except with custom User and Role types
// The key type is defined by TKey
public class IdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<
TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>,
IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
{
}
// No built-in Identity types are used; all are specified by generic arguments
// The key type is defined by TKey
public abstract class IdentityDbContext<
TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
: IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserRole : IdentityUserRole<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TRoleClaim : IdentityRoleClaim<TKey>
where TUserToken : IdentityUserToken<TKey>
It's also possible to use Identity without roles (only claims), in which case an IdentityUserContext<TUser> class
should be used:
// Uses the built-in non-role Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityUserContext<TUser>
: IdentityUserContext<TUser, string>
where TUser : IdentityUser
{
}
// Uses the built-in non-role Identity types except with a custom User type
// The key type is defined by TKey
public class IdentityUserContext<TUser, TKey> : IdentityUserContext<
TUser, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>,
IdentityUserToken<TKey>>
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
{
}
// No built-in Identity types are used; all are specified by generic arguments, with no roles
// The key type is defined by TKey
public abstract class IdentityUserContext<
TUser, TKey, TUserClaim, TUserLogin, TUserToken> : DbContext
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TUserToken : IdentityUserToken<TKey>
{
}
When overriding OnModelCreating , base.OnModelCreating should be called first; the overriding configuration
should be called next. EF Core generally has a last-one-wins policy for configuration. For example, if the ToTable
method for an entity type is called first with one table name and then again later with a different table name, the
table name in the second call is used.
Custom user data
Custom user data is supported by inheriting from IdentityUser . It's customary to name this type ApplicationUser
:
There's no need to override OnModelCreating in the ApplicationDbContext class. EF Core maps the CustomTag
property by convention. However, the database needs to be updated to create a new CustomTag column. To create
the column, add a migration, and then update the database as described in Identity and EF Core Migrations.
Update Startup.ConfigureServices to use the new ApplicationUser class:
services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI();
In ASP.NET Core 2.1 or later, Identity is provided as a Razor Class Library. For more information, see Scaffold
Identity in ASP.NET Core projects. Consequently, the preceding code requires a call to AddDefaultUI. If the Identity
scaffolder was used to add Identity files to the project, remove the call to AddDefaultUI . For more information, see:
Scaffold Identity
Add, download, and delete custom user data to Identity
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();
In the preceding code, the generic classes IdentityUser<TKey> and IdentityRole<TKey> must be specified to
use the new key type.
In the preceding code, the generic classes IdentityUser<TKey> and IdentityRole<TKey> must be specified to
use the new key type.
Startup.ConfigureServices must be updated to use the generic user:
services.AddDefaultIdentity<IdentityUser<Guid>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentity<IdentityUser<Guid>, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentity<IdentityUser<Guid>, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();
4. If a custom ApplicationUser class is being used, update the class to inherit from IdentityUser . For example:
using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System;
using Microsoft.AspNetCore.Identity;
Register the custom database context class when adding the Identity service in Startup.ConfigureServices :
services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
The primary key's data type is inferred by analyzing the DbContext object.
In ASP.NET Core 2.1 or later, Identity is provided as a Razor Class Library. For more information, see
Scaffold Identity in ASP.NET Core projects. Consequently, the preceding code requires a call to
AddDefaultUI. If the Identity scaffolder was used to add Identity files to the project, remove the call to
AddDefaultUI .
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
The primary key's data type is inferred by analyzing the DbContext object.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();
The AddEntityFrameworkStores method accepts a TKey type indicating the primary key's data type.
5. If a custom ApplicationRole class is being used, update the class to inherit from IdentityRole<TKey> . For
example:
using System;
using Microsoft.AspNetCore.Identity;
Update ApplicationDbContext to reference the custom ApplicationRole class. For example, the following
class references a custom ApplicationUser and a custom ApplicationRole :
using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
Register the custom database context class when adding the Identity service in Startup.ConfigureServices :
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
The primary key's data type is inferred by analyzing the DbContext object.
In ASP.NET Core 2.1 or later, Identity is provided as a Razor Class Library. For more information, see
Scaffold Identity in ASP.NET Core projects. Consequently, the preceding code requires a call to
AddDefaultUI. If the Identity scaffolder was used to add Identity files to the project, remove the call to
AddDefaultUI .
using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
Register the custom database context class when adding the Identity service in Startup.ConfigureServices :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
});
services.AddSingleton<IEmailSender, EmailSender>();
}
The primary key's data type is inferred by analyzing the DbContext object.
using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
Register the custom database context class when adding the Identity service in Startup.ConfigureServices :
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext, Guid>()
.AddDefaultTokenProviders();
services.AddMvc();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
The AddEntityFrameworkStores method accepts a TKey type indicating the primary key's data type.
Add navigation properties
Changing the model configuration for relationships can be more difficult than making other changes. Care must be
taken to replace the existing relationships rather than create new, additional relationships. In particular, the changed
relationship must specify the same foreign key (FK) property as the existing relationship. For example, the
relationship between Users and UserClaims is, by default, specified as follows:
builder.Entity<TUser>(b =>
{
// Each User can have many UserClaims
b.HasMany<TUserClaim>()
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
});
The FK for this relationship is specified as the UserClaim.UserId property. HasMany and WithOne are called without
arguments to create the relationship without navigation properties.
Add a navigation property to ApplicationUser that allows associated UserClaims to be referenced from the user:
The TKey for IdentityUserClaim<TKey> is the type specified for the PK of users. In this case, TKey is string
because the defaults are being used. It's not the PK type for the UserClaim entity type.
Now that the navigation property exists, it must be configured in OnModelCreating :
modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
});
}
}
Notice that relationship is configured exactly as it was before, only with a navigation property specified in the call
to HasMany .
The navigation properties only exist in the EF model, not the database. Because the FK for the relationship hasn't
changed, this kind of model change doesn't require the database to be updated. This can be checked by adding a
migration after making the change. The Up and Down methods are empty.
Add all User navigation properties
Using the section above as guidance, the following example configures unidirectional navigation properties for all
relationships on User:
public class ApplicationUser : IdentityUser
{
public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}
modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
// Each User can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne()
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}
}
modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(uc => uc.UserId)
.IsRequired();
// Each User can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
modelBuilder.Entity<ApplicationRole>(b =>
{
// Each Role can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
});
}
}
Notes:
This example also includes the UserRole join entity, which is needed to navigate the many-to-many relationship
from Users to Roles.
Remember to change the types of the navigation properties to reflect that ApplicationXxx types are now being
used instead of IdentityXxx types.
Remember to use the ApplicationXxx in the generic ApplicationContext definition.
Add all navigation properties
Using the section above as guidance, the following example configures navigation properties for all relationships
on all entity types:
modelBuilder.Entity<ApplicationUser>(b =>
{
// Each User can have many UserClaims
b.HasMany(e => e.Claims)
.WithOne(e => e.User)
.HasForeignKey(uc => uc.UserId)
.IsRequired();
// Each User can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
modelBuilder.Entity<ApplicationRole>(b =>
{
// Each Role can have many entries in the UserRole join table
b.HasMany(e => e.UserRoles)
.WithOne(e => e.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
modelBuilder.Entity<IdentityUser>(b =>
{
b.ToTable("MyUsers");
});
modelBuilder.Entity<IdentityUserClaim<string>>(b =>
{
b.ToTable("MyUserClaims");
});
modelBuilder.Entity<IdentityUserLogin<string>>(b =>
{
b.ToTable("MyUserLogins");
});
modelBuilder.Entity<IdentityUserToken<string>>(b =>
{
b.ToTable("MyUserTokens");
});
modelBuilder.Entity<IdentityRole>(b =>
{
b.ToTable("MyRoles");
});
modelBuilder.Entity<IdentityRoleClaim<string>>(b =>
{
b.ToTable("MyRoleClaims");
});
modelBuilder.Entity<IdentityUserRole<string>>(b =>
{
b.ToTable("MyUserRoles");
});
}
These examples use the default Identity types. If using an app type such as ApplicationUser , configure that type
instead of the default type.
The following example changes some column names:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityUser>(b =>
{
b.Property(e => e.Email).HasColumnName("EMail");
});
modelBuilder.Entity<IdentityUserClaim<string>>(b =>
{
b.Property(e => e.ClaimType).HasColumnName("CType");
b.Property(e => e.ClaimValue).HasColumnName("CValue");
});
}
Some types of database columns can be configured with certain facets (for example, the maximum string length
allowed). The following example sets column maximum lengths for several string properties in the model:
modelBuilder.Entity<IdentityUser>(b =>
{
b.Property(u => u.UserName).HasMaxLength(128);
b.Property(u => u.NormalizedUserName).HasMaxLength(128);
b.Property(u => u.Email).HasMaxLength(128);
b.Property(u => u.NormalizedEmail).HasMaxLength(128);
});
modelBuilder.Entity<IdentityUserToken<string>>(b =>
{
b.Property(t => t.LoginProvider).HasMaxLength(128);
b.Property(t => t.Name).HasMaxLength(128);
});
}
modelBuilder.HasDefaultSchema("notdbo");
}
Lazy loading
In this section, support for lazy-loading proxies in the Identity model is added. Lazy-loading is useful since it allows
navigation properties to be used without first ensuring they're loaded.
Entity types can be made suitable for lazy-loading in several ways, as described in the EF Core documentation. For
simplicity, use lazy-loading proxies, which requires:
Installation of the Microsoft.EntityFrameworkCore.Proxies package.
A call to UseLazyLoadingProxies inside AddDbContext<TContext>.
Public entity types with public virtual navigation properties.
The following example demonstrates calling UseLazyLoadingProxies in Startup.ConfigureServices :
services
.AddDbContext<ApplicationDbContext>(
b => b.UseSqlServer(connectionString)
.UseLazyLoadingProxies())
.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Refer to the preceding examples for guidance on adding navigation properties to the entity types.
Additional resources
Scaffold Identity in ASP.NET Core projects
Community OSS authentication options for ASP.NET
Core
6/21/2018 • 2 minutes to read • Edit Online
This page contains community-provided, open source authentication options for ASP.NET Core. This page is
periodically updated as new providers become available.
NAME DESCRIPTION
Gluu Server Enterprise ready, open source software for identity, access
management (IAM), and single sign-on (SSO). For more
information, see the Gluu Product Documentation.
ASP.NET Core Identity uses default values for settings such as password policy, lockout, and cookie configuration.
These settings can be overridden in the Startup class.
Identity options
The IdentityOptions class represents the options that can be used to configure the Identity system.
IdentityOptions must be set after calling AddIdentity or AddDefaultIdentity .
Claims Identity
IdentityOptions.ClaimsIdentity specifies the ClaimsIdentityOptions with the properties shown in the following
table.
SecurityStampClaimType Gets or sets the claim type used for the AspNet.Identity.SecurityStamp
security stamp claim.
UserIdClaimType Gets or sets the claim type used for the ClaimTypes.NameIdentifier
user identifier claim.
UserNameClaimType Gets or sets the claim type used for the ClaimTypes.Name
user name claim.
Lockout
Lockout is set in the PasswordSignInAsync method:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(Input.Email,
Input.Password, Input.RememberMe,
lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl,
Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
services.Configure<IdentityOptions>(options =>
{
// Default Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
The preceding code sets the IdentityOptions LockoutOptions with default values.
A successful authentication resets the failed access attempts count and resets the clock.
IdentityOptions.Lockout specifies the LockoutOptions with the properties shown in the table.
Password
By default, Identity requires that passwords contain an uppercase character, lowercase character, a digit, and a non-
alphanumeric character. Passwords must be at least six characters long. PasswordOptions can be set in
Startup.ConfigureServices .
services.Configure<IdentityOptions>(options =>
{
// Default Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
});
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
});
IdentityOptions.Password specifies the PasswordOptions with the properties shown in the table.
services.Configure<IdentityOptions>(options =>
{
// Default SignIn settings.
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
});
IdentityOptions.SignIn specifies the SignInOptions with the properties shown in the table.
Tokens
IdentityOptions.Tokens specifies the TokenOptions with the properties shown in the table.
PROPERTY DESCRIPTION
EmailConfirmationTokenProvider Gets or sets the token provider used to generate tokens used
in account confirmation emails.
ProviderMap Used to construct a User Token Provider with the key used as
the provider's name.
User
services.Configure<IdentityOptions>(options =>
{
// Default User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = true;
});
IdentityOptions.User specifies the UserOptions with the properties shown in the table.
Cookie settings
Configure the app's cookie in Startup.ConfigureServices . ConfigureApplicationCookie must be called after calling
AddIdentity or AddDefaultIdentity .
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.Cookie.Name = "YourAppCookieName";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.LoginPath = "/Identity/Account/Login";
// ReturnUrlParameter requires
//using Microsoft.AspNetCore.Authentication.Cookies;
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.SlidingExpiration = true;
});
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/Account/AccessDenied";
options.Cookie.Name = "YourAppCookieName";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.LoginPath = "/Account/Login";
// ReturnUrlParameter requires `using Microsoft.AspNetCore.Authentication.Cookies;`
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.SlidingExpiration = true;
});
services.Configure<IdentityOptions>(options =>
{
// Cookie settings
options.Cookies.ApplicationCookie.CookieName = "YourAppCookieName";
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/LogIn";
options.Cookies.ApplicationCookie.AccessDeniedPath = "/Account/AccessDenied";
options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
// Requires `using Microsoft.AspNetCore.Authentication.Cookies;`
options.Cookies.ApplicationCookie.AuthenticationScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.Cookies.ApplicationCookie.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
});
Windows Authentication
Windows Authentication relies on the operating system to authenticate users of ASP.NET Core apps. You can
use Windows Authentication when your server runs on a corporate network using Active Directory domain
identities or other Windows accounts to identify users. Windows Authentication is best suited to intranet
environments in which users, client applications, and web servers belong to the same Windows domain.
Learn more about Windows Authentication and installing it for IIS.
{
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:52171/",
"sslPort": 0
}
} // additional options trimmed
}
NOTE
HTTP.sys delegates to kernel mode authentication with the Kerberos authentication protocol. User mode authentication
isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket
that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the
Service Principal Name (SPN) for the host, not the user of the app.
host.Run();
}
}
NOTE
WebListener delegates to kernel mode authentication with the Kerberos authentication protocol. User mode
authentication isn't supported with Kerberos and WebListener. The machine account must be used to decrypt the
Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the
user. Register the Service Principal Name (SPN) for the host, not the user of the app.
Work with Windows Authentication
The configuration state of anonymous access determines the way in which the [Authorize] and
[AllowAnonymous] attributes are used in the app. The following two sections explain how to handle the
disallowed and allowed configuration states of anonymous access.
Disallow anonymous access
When Windows Authentication is enabled and anonymous access is disabled, the [Authorize] and
[AllowAnonymous] attributes have no effect. If the IIS site (or HTTP.sys or WebListener server ) is configured to
disallow anonymous access, the request never reaches your app. For this reason, the [AllowAnonymous] attribute
isn't applicable.
Allow anonymous access
When both Windows Authentication and anonymous access are enabled, use the [Authorize] and
[AllowAnonymous] attributes. The [Authorize] attribute allows you to secure pieces of the app which truly do
require Windows Authentication. The [AllowAnonymous] attribute overrides [Authorize] attribute usage within
apps which allow anonymous access. See Simple Authorization for attribute usage details.
In ASP.NET Core 2.x, the [Authorize] attribute requires additional configuration in Startup.cs to challenge
anonymous requests for Windows Authentication. The recommended configuration varies slightly based on the
web server being used.
NOTE
By default, users who lack authorization to access a page are presented with an empty HTTP 403 response. The
StatusCodePages middleware can be configured to provide users with a better "Access Denied" experience.
IIS
If using IIS, add the following to the ConfigureServices method:
HTTP.sys
If using HTTP.sys, add the following to the ConfigureServices method:
Impersonation
ASP.NET Core doesn't implement impersonation. Apps run with the application identity for all requests, using
app pool or process identity. If you need to explicitly perform an action on behalf of a user, use
WindowsIdentity.RunImpersonated . Run a single action in this context and then close the context.
app.Run(async (context) =>
{
try
{
var user = (WindowsIdentity)context.User.Identity;
await context.Response
.WriteAsync($"User: {user.Name}\tState: {user.ImpersonationLevel}\n");
WindowsIdentity.RunImpersonated(user.AccessToken, () =>
{
var impersonatedUser = WindowsIdentity.GetCurrent();
var message =
$"User: {impersonatedUser.Name}\tState: {impersonatedUser.ImpersonationLevel}";
Note that RunImpersonated doesn't support asynchronous operations and shouldn't be used for complex
scenarios. For example, wrapping entire requests or middleware chains isn't supported or recommended.
Custom storage providers for ASP.NET Core Identity
9/18/2018 • 9 minutes to read • Edit Online
By Steve Smith
ASP.NET Core Identity is an extensible system which enables you to create a custom storage provider and connect
it to your app. This topic describes how to create a customized storage provider for ASP.NET Core Identity. It
covers the important concepts for creating your own storage provider, but isn't a step-by-step walkthrough.
View or download sample from GitHub.
Introduction
By default, the ASP.NET Core Identity system stores user information in a SQL Server database using Entity
Framework Core. For many apps, this approach works well. However, you may prefer to use a different persistence
mechanism or data schema. For example:
You use Azure Table Storage or another data store.
Your database tables have a different structure.
You may wish to use a different data access approach, such as Dapper.
In each of these cases, you can write a customized provider for your storage mechanism and plug that provider
into your app.
ASP.NET Core Identity is included in project templates in Visual Studio with the "Individual User Accounts" option.
When using the .NET Core CLI, add -au Individual :
The implementation logic for creating the user is in the _usersTable.CreateAsync method, shown below.
if(rows > 0)
{
return IdentityResult.Success;
}
return IdentityResult.Failed(new IdentityError { Description = $"Could not insert user {user.Email}." });
}
using System;
namespace CustomIdentityProviderSample.CustomProvider
{
public class ApplicationRole
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; }
}
}
IRoleStore<TRole>
The IRoleStore<TRole> interface defines the methods to implement in the role store class. It contains methods
for creating, updating, deleting, and retrieving roles.
RoleStore<TRole>
To customize RoleStore , create a class that implements the IRoleStore<TRole> interface.
// Identity Services
services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
string connectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
services.AddTransient<DapperUsersTable>();
// additional configuration
}
References
Custom Storage Providers for ASP.NET 4.x Identity
ASP.NET Core Identity - This repository includes links to community maintained store providers.
Facebook, Google, and external provider
authentication in ASP.NET Core
8/31/2018 • 3 minutes to read • Edit Online
Enabling users to sign in with their existing credentials is convenient for the users and shifts many of the
complexities of managing the sign-in process onto a third party. For examples of how social logins can drive
traffic and customer conversions, see case studies by Facebook and Twitter.
Note: Packages presented here abstract a great deal of complexity of the OAuth authentication flow, but
understanding the details may become necessary when troubleshooting. Many resources are available; for
example, see Introduction to OAuth 2 or Understanding OAuth 2. Some issues can be resolved by looking at the
ASP.NET Core source code for the provider packages.
Tap Web Application and verify Authentication is set to Individual User Accounts:
Note: This tutorial applies to ASP.NET Core 2.0 SDK version which can be selected at the top of the wizard.
Apply migrations
Run the app and select the Log in link.
Select the Register as a new user link.
Enter the email and password for the new account, and then select Register.
Follow the instructions to apply migrations.
Require SSL
OAuth 2.0 requires the use of SSL for authentication over the HTTPS protocol.
Note: Projects created using Web Application or Web API project templates for ASP.NET Core 2.x are
automatically configured to enable SSL and launch with https URL if the Individual User Accounts option was
selected on Change Authentication dialog in the project wizard as shown above.
Require SSL on your site by following the steps in Enforce SSL in an ASP.NET Core app topic.
IMPORTANT
Secret Manager is for development purposes only. You can store and protect Azure test and production secrets with the
Azure Key Vault configuration provider.
Follow the steps in Safe storage of app secrets in development in ASP.NET Core topic to store tokens assigned
by each login provider below.
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
Tap Create
Set a valid password and you can use this to sign in with your email.
Next steps
This article introduced external authentication and explained the prerequisites required to add external
logins to your ASP.NET Core app.
Reference provider-specific pages to configure logins for the providers required by your app.
You may want to persist additional data about the user and their access and refresh tokens. For more
information, see Persist additional claims and tokens from external providers in ASP.NET Core.
Facebook external login setup in ASP.NET Core
8/22/2018 • 4 minutes to read • Edit Online
Fill out the form and tap the Create App ID button.
On the Select a product page, click Set Up on the Facebook Login card.
The Quickstart wizard will launch with Choose a Platform as the first page. Bypass the wizard for now by
clicking the Settings link in the menu on the left:
NOTE
The URI /signin-facebook is set as the default callback of the Facebook authentication provider. You can change the default
callback URI while configuring the Facebook authentication middleware via the inherited
RemoteAuthenticationOptions.CallbackPath property of the FacebookOptions class.
Execute the following commands to securely store App ID and App Secret using Secret Manager:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddFacebook(facebookOptions =>
{
facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
});
The call to AddIdentity configures the default scheme settings. The AddAuthentication(String) overload sets the
DefaultScheme property. The AddAuthentication(Action<AuthenticationOptions>) overload allows configuring
authentication options, which can be used to set up default authentication schemes for different purposes.
Subsequent calls to AddAuthentication override previously configured AuthenticationOptions properties.
AuthenticationBuilder extension methods that register an authentication handler may only be called once per
authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display
name.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
See the FacebookOptions API reference for more information on configuration options supported by Facebook
authentication. Configuration options can be used to:
Request different information about the user.
Add query string arguments to customize the login experience.
Troubleshooting
ASP.NET Core 2.x only: If Identity isn't configured by calling services.AddIdentity in ConfigureServices ,
attempting to authenticate will result in ArgumentException: The 'SignInScheme' option must be provided. The
project template used in this tutorial ensures that this is done.
If the site database has not been created by applying the initial migration, you get A database operation failed
while processing the request error. Tap Apply Migrations to create the database and refresh to continue past
the error.
Next steps
This article showed how you can authenticate with Facebook. You can follow a similar approach to
authenticate with other providers listed on the previous page.
Once you publish your web site to Azure web app, you should reset the AppSecret in the Facebook
developer portal.
Set the Authentication:Facebook:AppId and Authentication:Facebook:AppSecret as application settings in
the Azure portal. The configuration system is set up to read keys from environment variables.
Twitter external login setup with ASP.NET Core
6/21/2018 • 3 minutes to read • Edit Online
Tap Create New App and fill out the application Name, Description and public Website URI (this can be
temporary until you register the domain name):
Enter your development URI with /signin-twitter appended into the Valid OAuth Redirect URIs field (for
example: https://localhost:44320/signin-twitter ). The Twitter authentication scheme configured later in this
tutorial will automatically handle requests at /signin-twitter route to implement the OAuth flow.
NOTE
The URI segment /signin-twitter is set as the default callback of the Twitter authentication provider. You can change the
default callback URI while configuring the Twitter authentication middleware via the inherited
RemoteAuthenticationOptions.CallbackPath property of the TwitterOptions class.
Fill out the rest of the form and tap Create your Twitter application. New application details are displayed:
When deploying the site you'll need to revisit the Application Management page and register a new public
URI.
These tokens can be found on the Keys and Access Tokens tab after creating your new Twitter application:
Configure Twitter Authentication
The project template used in this tutorial ensures that Microsoft.AspNetCore.Authentication.Twitter package is
already installed.
To install this package with Visual Studio 2017, right-click on the project and select Manage NuGet
Packages.
To install with .NET Core CLI, execute the following in your project directory:
dotnet add package Microsoft.AspNetCore.Authentication.Twitter
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddTwitter(twitterOptions =>
{
twitterOptions.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
twitterOptions.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
});
The call to AddIdentity configures the default scheme settings. The AddAuthentication(String) overload sets the
DefaultScheme property. The AddAuthentication(Action<AuthenticationOptions>) overload allows configuring
authentication options, which can be used to set up default authentication schemes for different purposes.
Subsequent calls to AddAuthentication override previously configured AuthenticationOptions properties.
AuthenticationBuilder extension methods that register an authentication handler may only be called once per
authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display
name.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
See the TwitterOptions API reference for more information on configuration options supported by Twitter
authentication. This can be used to request different information about the user.
Troubleshooting
ASP.NET Core 2.x only: If Identity isn't configured by calling services.AddIdentity in ConfigureServices ,
attempting to authenticate will result in ArgumentException: The 'SignInScheme' option must be provided. The
project template used in this tutorial ensures that this is done.
If the site database has not been created by applying the initial migration, you will get A database operation
failed while processing the request error. Tap Apply Migrations to create the database and refresh to continue
past the error.
Next steps
This article showed how you can authenticate with Twitter. You can follow a similar approach to
authenticate with other providers listed on the previous page.
Once you publish your web site to Azure web app, you should reset the ConsumerSecret in the Twitter
developer portal.
Set the Authentication:Twitter:ConsumerKey and Authentication:Twitter:ConsumerSecret as application
settings in the Azure portal. The configuration system is set up to read keys from environment variables.
Google external login setup in ASP.NET Core
6/21/2018 • 4 minutes to read • Edit Online
After accepting the dialog, you are redirected back to the Library page allowing you to choose features for your
new app. Find Google+ API in the list and click on its link to add the API feature:
The page for the newly added API is displayed. Tap Enable to add Google+ sign in feature to your app:
After enabling the API, tap Create credentials to configure the secrets:
Choose:
Google+ API
Web server (e.g. node.js, Tomcat), and
User data:
Tap What credentials do I need? which takes you to the second step of app configuration, Create an OAuth
2.0 client ID:
Because we are creating a Google+ project with just one feature (sign in), we can enter the same Name for
the OAuth 2.0 client ID as the one we used for the project.
Enter your development URI with /signin-google appended into the Authorized redirect URIs field (for
example: https://localhost:44320/signin-google ). The Google authentication configured later in this
tutorial will automatically handle requests at /signin-google route to implement the OAuth flow.
NOTE
The URI segment /signin-google is set as the default callback of the Google authentication provider. You can change the
default callback URI while configuring the Google authentication middleware via the inherited
RemoteAuthenticationOptions.CallbackPath property of the GoogleOptions class.
The values for these tokens can be found in the JSON file downloaded in the previous step under web.client_id
and web.client_secret .
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});
The call to AddIdentity configures the default scheme settings. The AddAuthentication(String) overload sets the
DefaultScheme property. The AddAuthentication(Action<AuthenticationOptions>) overload allows configuring
authentication options, which can be used to set up default authentication schemes for different purposes.
Subsequent calls to AddAuthentication override previously configured AuthenticationOptions properties.
AuthenticationBuilder extension methods that register an authentication handler may only be called once per
authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display
name.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
See the GoogleOptions API reference for more information on configuration options supported by Google
authentication. This can be used to request different information about the user.
When you click on Google, you are redirected to Google for authentication:
After entering your Google credentials, then you are redirected back to the web site where you can set your email.
You are now logged in using your Google credentials:
Troubleshooting
If you receive a 403 (Forbidden) error page from your own app when running in development mode (or break
into the debugger with the same error), ensure that Google+ API has been enabled in the API Manager
Library by following the steps listed earlier on this page. If the sign in doesn't work and you aren't getting any
errors, switch to development mode to make the issue easier to debug.
ASP.NET Core 2.x only: If Identity isn't configured by calling services.AddIdentity in ConfigureServices ,
attempting to authenticate will result in ArgumentException: The 'SignInScheme' option must be provided. The
project template used in this tutorial ensures that this is done.
If the site database has not been created by applying the initial migration, you will get A database operation
failed while processing the request error. Tap Apply Migrations to create the database and refresh to continue
past the error.
Next steps
This article showed how you can authenticate with Google. You can follow a similar approach to
authenticate with other providers listed on the previous page.
Once you publish your web site to Azure web app, you should reset the ClientSecret in the Google API
Console.
Set the Authentication:Google:ClientId and Authentication:Google:ClientSecret as application settings in
the Azure portal. The configuration system is set up to read keys from environment variables.
Microsoft Account external login setup with ASP.NET
Core
6/21/2018 • 4 minutes to read • Edit Online
If you don't already have a Microsoft account, tap Create one! After signing in you are redirected to My
applications page:
Tap Add an app in the upper right corner and enter your Application Name and Contact Email:
For the purposes of this tutorial, clear the Guided Setup check box.
Tap Create to continue to the Registration page. Provide a Name and note the value of the Application
Id, which you use as ClientId later in the tutorial:
Tap Add Platform in the Platforms section and select the Web platform:
In the new Web platform section, enter your development URL with /signin-microsoft appended into the
Redirect URLs field (for example: https://localhost:44320/signin-microsoft ). The Microsoft authentication
scheme configured later in this tutorial will automatically handle requests at /signin-microsoft route to
implement the OAuth flow:
NOTE
The URI segment /signin-microsoft is set as the default callback of the Microsoft authentication provider. You can
change the default callback URI while configuring the Microsoft authentication middleware via the inherited
RemoteAuthenticationOptions.CallbackPath property of the MicrosoftAccountOptions class.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ApplicationId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:Password"];
});
The call to AddIdentity configures the default scheme settings. The AddAuthentication(String) overload sets the
DefaultScheme property. The AddAuthentication(Action<AuthenticationOptions>) overload allows configuring
authentication options, which can be used to set up default authentication schemes for different purposes.
Subsequent calls to AddAuthentication override previously configured AuthenticationOptions properties.
AuthenticationBuilder extension methods that register an authentication handler may only be called once per
authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display
name.
When the app requires multiple providers, chain the provider extension methods behind AddAuthentication:
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
Although the terminology used on Microsoft Developer Portal names these tokens ApplicationId and Password ,
they're exposed as ClientId and ClientSecret to the configuration API.
See the MicrosoftAccountOptions API reference for more information on configuration options supported by
Microsoft Account authentication. This can be used to request different information about the user.
When you click on Microsoft, you are redirected to Microsoft for authentication. After signing in with your
Microsoft Account (if not already signed in) you will be prompted to let the app access your info:
Tap Yes and you will be redirected back to the web site where you can set your email.
You are now logged in using your Microsoft credentials:
Troubleshooting
If the Microsoft Account provider redirects you to a sign in error page, note the error title and description
query string parameters directly following the # (hashtag) in the Uri.
Although the error message seems to indicate a problem with Microsoft authentication, the most common
cause is your application Uri not matching any of the Redirect URIs specified for the Web platform.
ASP.NET Core 2.x only: If Identity isn't configured by calling services.AddIdentity in ConfigureServices ,
attempting to authenticate will result in ArgumentException: The 'SignInScheme' option must be provided.
The project template used in this tutorial ensures that this is done.
If the site database has not been created by applying the initial migration, you will get A database
operation failed while processing the request error. Tap Apply Migrations to create the database and
refresh to continue past the error.
Next steps
This article showed how you can authenticate with Microsoft. You can follow a similar approach to
authenticate with other providers listed on the previous page.
Once you publish your web site to Azure web app, you should create a new Password in the Microsoft
Developer Portal.
Set the Authentication:Microsoft:ApplicationId and Authentication:Microsoft:Password as application
settings in the Azure portal. The configuration system is set up to read keys from environment variables.
Short survey of other authentication providers
6/21/2018 • 2 minutes to read • Edit Online
services.AddAuthentication()
.AddMicrosoftAccount(microsoftOptions => { ... })
.AddGoogle(googleOptions => { ... })
.AddTwitter(twitterOptions => { ... })
.AddFacebook(facebookOptions => { ... });
Persist additional claims and tokens from external
providers in ASP.NET Core
8/31/2018 • 6 minutes to read • Edit Online
By Luke Latham
An ASP.NET Core app can establish additional claims and tokens from external authentication providers, such as
Facebook, Google, Microsoft, and Twitter. Each provider reveals different information about users on its platform,
but the pattern for receiving and transforming user data into additional claims is the same.
View or download sample code (how to download)
Prerequisite
Decide which external authentication providers to support in the app. For each provider, register the app and obtain
a client ID and client secret. For more information, see Facebook, Google, and external provider authentication in
ASP.NET Core. The sample app uses the Google authentication provider.
PROVIDER SCOPE
Facebook https://www.facebook.com/dialog/oauth
Google https://www.googleapis.com/auth/plus.login
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate
The sample app adds the Google plus.login scope to request Google+ sign in permissions:
services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
Map user data keys and create claims
In the provider's options, specify a MapJsonKey for each key in the external provider's JSON user data for the app
identity to read on sign in. For more information on claim types, see ClaimTypes.
The sample app creates a Gender claim from the gender key in Google user data:
services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
In OnPostConfirmationAsync, an IdentityUser ( ApplicationUser ) is signed into the app with SignInAsync. During
the sign in process, the UserManager<TUser> can store an ApplicationUser claim for user data available from the
Principal.
In the sample app, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) establishes a Gender claim for the
signed in ApplicationUser :
public async Task<IActionResult> OnPostConfirmationAsync(
string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login
// provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
throw new ApplicationException(
"Error loading external login data during confirmation.");
}
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
// Copy over the gender claim
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst(ClaimTypes.Gender));
return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
}
ReturnUrl = returnUrl;
return Page();
}
When OnPostConfirmationAsync executes, store the access token (ExternalLoginInfo.AuthenticationTokens) from the
external provider in the ApplicationUser 's AuthenticationProperties .
The sample app saves the access token in:
OnPostConfirmationAsync – Executes for new user registration.
OnGetCallbackAsync – Executes when a previously registered user signs into the app.
Account/ExternalLogin.cshtml.cs:
public async Task<IActionResult> OnPostConfirmationAsync(
string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login
// provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
throw new ApplicationException(
"Error loading external login data during confirmation.");
}
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
// Copy over the gender claim
await _userManager.AddClaimAsync(user,
info.Principal.FindFirst(ClaimTypes.Gender));
return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
}
ReturnUrl = returnUrl;
return Page();
}
public async Task<IActionResult> OnGetCallbackAsync(
string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Login");
}
if (info == null)
{
return RedirectToPage("./Login");
}
// Sign in the user with this external login provider if the user
// already has a login
var result = await _signInManager.ExternalLoginSignInAsync(
info.LoginProvider, info.ProviderKey, isPersistent: false,
bypassTwoFactor : true);
if (result.Succeeded)
{
// Store the access token and resign in so the token is included in
// in the cookie
var user = await _userManager.FindByLoginAsync(info.LoginProvider,
info.ProviderKey);
_logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.",
info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
if (result.IsLockedOut)
{
return RedirectToPage("./Lockout");
}
else
{
// If the user does not have an account, then ask the user to
// create an account
ReturnUrl = returnUrl;
LoginProvider = info.LoginProvider;
return Page();
}
}
services.AddAuthentication().AddGoogle(options =>
{
// Provide the Google Client ID
options.ClientId = "XXXXXXXXXXX.apps.googleusercontent.com";
// Provide the Google Secret
options.ClientSecret = "g4GZ2#...GD5Gg1x";
options.Scope.Add("https://www.googleapis.com/auth/plus.login");
options.ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
options.SaveTokens = true;
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens()
as List<AuthenticationToken>;
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
b36a7b09-9135-4810-b7a5-78697ff23e99
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
[email protected]
AspNet.Identity.SecurityStamp
29G2TB881ATCUQFJSRFG1S0QJ0OOAWVT
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender
female
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod
Google
Authentication Properties
.Token.access_token
bv42.Dgw...GQMv9ArLPs
.Token.token_type
Bearer
.Token.expires_at
2018-08-27T19:08:00.0000000+00:00
.Token.TicketCreated
8/27/2018 6:08:00 PM
.TokenNames
access_token;token_type;expires_at;TicketCreated
.issued
Mon, 27 Aug 2018 18:08:05 GMT
.expires
Mon, 10 Sep 2018 18:08:05 GMT
Authenticate users with WS-Federation in ASP.NET
Core
6/21/2018 • 3 minutes to read • Edit Online
This tutorial demonstrates how to enable users to sign in with a WS -Federation authentication provider like Active
Directory Federation Services (ADFS ) or Azure Active Directory (AAD ). It uses the ASP.NET Core 2.0 sample app
described in Facebook, Google, and external provider authentication.
For ASP.NET Core 2.0 apps, WS -Federation support is provided by
Microsoft.AspNetCore.Authentication.WsFederation. This component is ported from
Microsoft.Owin.Security.WsFederation and shares many of that component's mechanics. However, the
components differ in a couple of important ways.
By default, the new middleware:
Doesn't allow unsolicited logins. This feature of the WS -Federation protocol is vulnerable to XSRF attacks.
However, it can be enabled with the AllowUnsolicitedLogins option.
Doesn't check every form post for sign-in messages. Only requests to the CallbackPath are checked for sign-
ins. CallbackPath defaults to /signin-wsfed but can be changed via the inherited
RemoteAuthenticationOptions.CallbackPath property of the WsFederationOptions class. This path can be
shared with other authentication providers by enabling the SkipUnrecognizedRequests option.
NOTE
This must be an HTTPS URL. IIS Express can provide a self-signed certificate when hosting the app during development.
Kestrel requires manual certificate configuration. See the Kestrel documentation for more details.
Click Next through the rest of the wizard and Close at the end.
ASP.NET Core Identity requires a Name ID claim. Add one from the Edit Claim Rules dialog:
In the Add Transform Claim Rule Wizard, leave the default Send LDAP Attributes as Claims template
selected, and click Next. Add a rule mapping the SAM -Account-Name LDAP attribute to the Name ID
outgoing claim:
Click Finish > OK in the Edit Claim Rules window.
Azure Active Directory
Navigate to the AAD tenant's app registrations blade. Click New application registration:
Enter a name for the app registration. This isn't important to the ASP.NET Core app.
Enter the URL the app listens on as the Sign-on URL:
Click Endpoints and note the Federation Metadata Document URL. This is the WS -Federation
middleware's MetadataAddress :
Navigate to the new app registration. Click Settings > Properties and make note of the App ID URI. This is
the WS -Federation middleware's Wtrealm :
Add WS-Federation as an external login provider for ASP.NET Core
Identity
Add a dependency on Microsoft.AspNetCore.Authentication.WsFederation to the project.
Add WS -Federation to the Configure method in Startup.cs:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddWsFederation(options =>
{
// MetadataAddress represents the Active Directory instance used to authenticate users.
options.MetadataAddress = "https://<ADFS FQDN or AAD tenant>/FederationMetadata/2007-
06/FederationMetadata.xml";
// For AAD, use the App ID URI from the app registration's Properties blade:
options.Wtrealm = "https://wsfedsample.onmicrosoft.com/bf0e7e6d-056e-4e37-b9a6-2c36797b9f01";
});
services.AddMvc()
// ...
The call to AddIdentity configures the default scheme settings. The AddAuthentication(String) overload sets the
DefaultScheme property. The AddAuthentication(Action<AuthenticationOptions>) overload allows configuring
authentication options, which can be used to set up default authentication schemes for different purposes.
Subsequent calls to AddAuthentication override previously configured AuthenticationOptions properties.
AuthenticationBuilder extension methods that register an authentication handler may only be called once per
authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display
name.
Log in with WS -Federation
Browse to the app and click the Log in link in the nav header. There's an option to log in with WsFederation:
With ADFS as the provider, the button redirects to an ADFS sign-in page:
With Azure Active Directory as the provider, the button redirects to an AAD sign-in page:
A successful sign-in for a new user redirects to the app's user registration page:
See this PDF file for the ASP.NET Core 1.1 and 2.1 version.
Prerequisites
.NET Core 2.1 SDK or later
services.AddDefaultIdentity<IdentityUser>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<WebPWrecoverContext>();
});
}
}
config.SignIn.RequireConfirmedEmail = true; prevents registered users from logging in until their email is
confirmed.
Configure email provider
In this tutorial, SendGrid is used to send email. You need a SendGrid account and key to send email. You can use
other email providers. ASP.NET Core 2.x includes System.Net.Mail , which allows you to send email from your
app. We recommend you use SendGrid or another email service to send email. SMTP is difficult to secure and
set up correctly.
Create a class to fetch the secure email key. For this sample, create Services/AuthMessageSenderOptions.cs:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>aspnet-WebPWrecover-1234</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.4" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
<PackageReference Include="SendGrid" Version="9.9.0" />
</ItemGroup>
</Project>
Set the SendGridUser and SendGridKey with the secret-manager tool. For example:
{
"SendGridUser": "RickAndMSFT",
"SendGridKey": "<key removed>"
}
Install-Package SendGrid
See Get Started with SendGrid for Free to register for a free SendGrid account.
Implement IEmailSender
To Implement IEmailSender , create Services/EmailSender.cs with code similar to the following:
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;
namespace WebPWrecover.Services
{
public class EmailSender : IEmailSender
{
public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
public Task Execute(string apiKey, string subject, string message, string email)
{
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage()
{
From = new EmailAddress("[email protected]", "Joe Smith"),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
return client.SendEmailAsync(msg);
}
}
}
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// requires
// using Microsoft.AspNetCore.Identity.UI.Services;
// using WebPWrecover.Services;
services.AddSingleton<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
}
Click the link to another login service and accept the app requests. In the following image, Facebook is the
external authentication provider:
The two accounts have been combined. You are able to log on with either account. You might want your users to
add local accounts in case their social login authentication service is down, or more likely they've lost access to
their social account.
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
Update the Scripts section to add a reference to the qrcodejs library you added and a call to generate the
QR Code. It should look as follows:
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
The second parameter in the call to string.Format is your site name, taken from your solution name. It can be
changed to any value, but it must always be URL encoded.
WARNING
Two factor authentication (2FA) authenticator apps, using a Time-based One-time Password Algorithm (TOTP), are the
industry recommended approach for 2FA. 2FA using TOTP is preferred to SMS 2FA. For more information, see Enable QR
Code generation for TOTP authenticator apps in ASP.NET Core for ASP.NET Core 2.0 and later.
This tutorial shows how to set up two-factor authentication (2FA) using SMS. Instructions are given for twilio and
ASPSMS, but you can use any other SMS provider. We recommend you complete Account Confirmation and
Password Recovery before starting this tutorial.
View the completed sample. How to download.
Set the SMSAccountIdentification , SMSAccountPassword and SMSAccountFrom with the secret-manager tool. For
example:
Add the NuGet package for the SMS provider. From the Package Manager Console (PMC ) run:
Twilio: Install-Package Twilio
Add code in the Services/MessageServices.cs file to enable SMS. Use either the Twilio or the ASPSMS section:
Twilio:
using Microsoft.Extensions.Options;
using System.Threading.Tasks;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
namespace Web2FA.Services
{
// This class is used by the application to send Email and SMS
// when you turn on two-factor authentication in ASP.NET Identity.
// For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713
public class AuthMessageSender : IEmailSender, ISmsSender
{
public AuthMessageSender(IOptions<SMSoptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
TwilioClient.Init(accountSid, authToken);
return MessageResource.CreateAsync(
to: new PhoneNumber(number),
from: new PhoneNumber(Options.SMSAccountFrom),
body: message);
}
}
}
ASPSMS:
using Microsoft.Extensions.Options;
using System.Threading.Tasks;
namespace Web2FA.Services
{
// This class is used by the application to send Email and SMS
// when you turn on two-factor authentication in ASP.NET Identity.
// For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713
public class AuthMessageSender : IEmailSender, ISmsSender
{
public AuthMessageSender(IOptions<SMSoptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
SMSSender.Userkey = Options.SMSAccountIdentification;
SMSSender.Password = Options.SMSAccountPassword;
SMSSender.Originator = Options.SMSAccountFrom;
SMSSender.AddRecipient(number);
SMSSender.MessageData = message;
SMSSender.SendTextSMS();
return Task.FromResult(0);
}
}
}
Add SMSoptions to the service container in the ConfigureServices method in the Startup.cs:
Add a phone number that will receive the verification code, and tap Send verification code.
You will get a text message with the verification code. Enter it and tap Submit
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
services.Configure<IdentityOptions>(options =>
{
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
});
Configuration
ASP.NET Core 2.x
ASP.NET Core 1.x
If the app doesn't use the Microsoft.AspNetCore.App metapackage, create a package reference in the project file
for the Microsoft.AspNetCore.Authentication.Cookies package (version 2.1.0 or later).
In the ConfigureServices method, create the Authentication Middleware service with the AddAuthentication and
AddCookie methods:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
AuthenticationScheme passed to AddAuthentication sets the default authentication scheme for the app.
AuthenticationScheme is useful when there are multiple instances of cookie authentication and you want to
authorize with a specific scheme. Setting the AuthenticationScheme to
CookieAuthenticationDefaults.AuthenticationScheme provides a value of "Cookies" for the scheme. You can supply
any string value that distinguishes the scheme.
The app's authentication scheme is different from the app's cookie authentication scheme. When a cookie
authentication scheme isn't provided to AddCookie, it uses CookieAuthenticationDefaults.AuthenticationScheme
("Cookies").
In the Configure method, use the UseAuthentication method to invoke the Authentication Middleware that sets
the HttpContext.User property. Call the UseAuthentication method before calling UseMvcWithDefaultRoute or
UseMvc :
app.UseAuthentication();
AddCookie Options
The CookieAuthenticationOptions class is used to configure the authentication provider options.
OPTION DESCRIPTION
AccessDeniedPath Provides the path to supply with a 302 Found (URL redirect)
when triggered by HttpContext.ForbidAsync . The default
value is /Account/AccessDenied .
ClaimsIssuer The issuer to use for the Issuer property on any claims
created by the cookie authentication service.
Cookie.Domain The domain name where the cookie is served. By default, this
is the host name of the request. The browser only sends the
cookie in requests to a matching host name. You may wish to
adjust this to have cookies available to any host in your
domain. For example, setting the cookie domain to
.contoso.com makes it available to contoso.com ,
www.contoso.com , and staging.www.contoso.com .
Cookie.Path Used to isolate apps running on the same host name. If you
have an app running at /app1 and want to restrict cookies
to that app, set the CookiePath property to /app1 . By
doing so, the cookie is only available on requests to /app1
and any app underneath it.
Events The handler calls methods on the provider that give the app
control at certain processing points. If Events aren't
provided, a default instance is supplied that does nothing
when the methods are called.
EventsType Used as the service type to get the Events instance instead
of the property.
LoginPath Provides the path to supply with a 302 Found (URL redirect)
when triggered by HttpContext.ChallengeAsync . The
current URL that generated the 401 is added to the
LoginPath as a query string parameter named by the
ReturnUrlParameter . Once a request to the LoginPath
grants a new sign-in identity, the ReturnUrlParameter value
is used to redirect the browser back to the URL that caused
the original unauthorized status code. The default value is
/Account/Login .
Set CookieAuthenticationOptions in the service configuration for authentication in the ConfigureServices method:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
...
});
app.UseCookiePolicy(cookiePolicyOptions);
The CookiePolicyOptions provided to the Cookie Policy Middleware allow you to control global characteristics of
cookie processing and hook into cookie processing handlers when cookies are appended or deleted.
PROPERTY DESCRIPTION
The Cookie Policy Middleware setting for MinimumSameSitePolicy can affect your setting of Cookie.SameSite in
CookieAuthenticationOptions settings according to the matrix below.
//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
//IsPersistent = true,
// Whether the authentication session is persisted across
// multiple requests. Required when setting the
// ExpireTimeSpan option of CookieAuthenticationOptions
// set with AddCookie. Also required when setting
// ExpiresUtc.
//IssuedUtc = <DateTimeOffset>,
// The time at which the authentication ticket was issued.
//RedirectUri = <string>
// The full path or absolute URI to be used as an http
// redirect response value.
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
SignInAsync creates an encrypted cookie and adds it to the current response. If you don't specify an
AuthenticationScheme , the default scheme is used.
Under the covers, the encryption used is ASP.NET Core's Data Protection system. If you're hosting app on
multiple machines, load balancing across apps, or using a web farm, then you must configure data protection to
use the same key ring and app identifier.
Sign out
ASP.NET Core 2.x
ASP.NET Core 1.x
To sign out the current user and delete their cookie, call SignOutAsync:
await HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
If you aren't using CookieAuthenticationDefaults.AuthenticationScheme (or "Cookies") as the scheme (for example,
"ContosoCookie"), supply the scheme you used when configuring the authentication provider. Otherwise, the
default scheme is used.
React to back-end changes
Once a cookie is created, it becomes the single source of identity. Even if you disable a user in your back-end
systems, the cookie authentication system has no knowledge of this, and a user stays logged in as long as their
cookie is valid.
The ValidatePrincipal event in ASP.NET Core 2.x or the ValidateAsync method in ASP.NET Core 1.x can be used
to intercept and override validation of the cookie identity. This approach mitigates the risk of revoked users
accessing the app.
One approach to cookie validation is based on keeping track of when the user database has been changed. If the
database hasn't been changed since the user's cookie was issued, there's no need to re-authenticate the user if
their cookie is still valid. To implement this scenario, the database, which is implemented in IUserRepository for
this example, stores a LastChanged value. When any user is updated in the database, the LastChanged value is set
to the current time.
In order to invalidate a cookie when the database changes based on the LastChanged value, create the cookie with
a LastChanged claim containing the current LastChanged value from the database:
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));
ValidatePrincipal(CookieValidatePrincipalContext)
if (string.IsNullOrEmpty(lastChanged) ||
!_userRepository.ValidateLastChanged(lastChanged))
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}
Register the events instance during cookie service registration in the ConfigureServices method. Provide a
scoped service registration for your CustomCookieAuthenticationEvents class:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
services.AddScoped<CustomCookieAuthenticationEvents>();
Consider a situation in which the user's name is updated — a decision that doesn't affect security in any way. If
you want to non-destructively update the user principal, call context.ReplacePrincipal and set the
context.ShouldRenew property to true .
WARNING
The approach described here is triggered on every request. This can result in a large performance penalty for the app.
Persistent cookies
You may want the cookie to persist across browser sessions. This persistence should only be enabled with explicit
user consent with a "Remember Me" checkbox on login or a similar mechanism.
The following code snippet creates an identity and corresponding cookie that survives through browser closures.
Any sliding expiration settings previously configured are honored. If the cookie expires while the browser is
closed, the browser clears the cookie once it's restarted.
ASP.NET Core 2.x
ASP.NET Core 1.x
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true
});
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
});
Additional resources
Auth 2.0 Changes / Migration Announcement
Authorize with a specific scheme in ASP.NET Core
Claims-based authorization in ASP.NET Core
Policy-based role checks
Host ASP.NET Core in a web farm
Azure Active Directory with ASP.NET Core
6/28/2018 • 2 minutes to read • Edit Online
Azure AD V1 samples
The following samples show how to integrate Azure AD V1, enabling users to sign-in with a work and school
account:
Integrating Azure AD Into an ASP.NET Core Web App
Calling a ASP.NET Core Web API From a WPF Application Using Azure AD
Calling a Web API in an ASP.NET Core Web Application Using Azure AD
Azure AD V2 samples
The following samples show how to integrate Azure AD V2, enabling users to sign-in with a work and school
account or a Microsoft personal account (formely Live account):
Integrating Azure AD V2 into an ASP.NET Core 2.0 web app:
See this associated video
Calling a ASP.NET Core 2.0 Web API from a WPF application using Azure AD V2:
See this associated video
By Cam Soper
Azure Active Directory B2C (Azure AD B2C ) is a cloud identity management solution for web and mobile apps.
The service provides authentication for apps hosted in the cloud and on-premises. Authentication types include
individual accounts, social network accounts, and federated enterprise accounts. Additionally, Azure AD B2C can
provide multi-factor authentication with minimal configuration.
TIP
Azure Active Directory (Azure AD) and Azure AD B2C are separate product offerings. An Azure AD tenant represents an
organization, while an Azure AD B2C tenant represents a collection of identities to be used with relying party applications. To
learn more, see Azure AD B2C: Frequently asked questions (FAQ).
Prerequisites
The following are required for this walkthrough:
Microsoft Azure subscription
Visual Studio 2017 (any edition)
WARNING
If setting up a non-localhost Reply URL, be aware of the constraints on what is allowed in the Reply URL list.
After the app is registered, the list of apps in the tenant is displayed. Select the app that was just registered. Select
the Copy icon to the right of the Application ID field to copy it to the clipboard.
Nothing more can be configured in the Azure AD B2C tenant at this time, but leave the browser window open.
There is more configuration after the ASP.NET Core app is created.
SETTING VALUE
Select the Copy link next to Reply URI to copy the Reply URI to the clipboard. Select OK to close the
Change Authentication dialog. Select OK to create the web app.
TIP
If you didn't copy the Reply URL, use the SSL address from the Debug tab in the web project properties, and append the
CallbackPath value from appsettings.json.
Configure policies
Use the steps in the Azure AD B2C documentation to create a sign-up or sign-in policy, and then create a
password reset policy. Use the example values provided in the documentation for Identity providers, Sign-up
attributes, and Application claims. Using the Run now button to test the policies as described in the
documentation is optional.
WARNING
Ensure the policy names are exactly as described in the documentation, as those policies were used in the Change
Authentication dialog in Visual Studio. The policy names can be verified in appsettings.json.
Next steps
In this tutorial, you learned how to:
Create an Azure Active Directory B2C tenant
Register an app in Azure AD B2C
Use Visual Studio to create an ASP.NET Core Web Application configured to use the Azure AD B2C tenant for
authentication
Configure policies controlling the behavior of the Azure AD B2C tenant
Now that the ASP.NET Core app is configured to use Azure AD B2C for authentication, the Authorize attribute can
be used to secure your app. Continue developing your app by learning to:
Customize the Azure AD B2C user interface.
Configure password complexity requirements.
Enable multi-factor authentication.
Configure additional identity providers, such as Microsoft, Facebook, Google, Amazon, Twitter, and others.
Use the Azure AD Graph API to retrieve additional user information, such as group membership, from the
Azure AD B2C tenant.
Secure an ASP.NET Core web API using Azure AD B2C.
Call a .NET web API from a .NET web app using Azure AD B2C.
Cloud authentication in web APIs with Azure Active
Directory B2C in ASP.NET Core
9/24/2018 • 7 minutes to read • Edit Online
By Cam Soper
Azure Active Directory B2C (Azure AD B2C ) is a cloud identity management solution for web and mobile apps.
The service provides authentication for apps hosted in the cloud and on-premises. Authentication types include
individual accounts, social network accounts, and federated enterprise accounts. Additionally, Azure AD B2C can
provide multi-factor authentication with minimal configuration.
TIP
Azure Active Directory (Azure AD) and Azure AD B2C are separate product offerings. An Azure AD tenant represents an
organization, while an Azure AD B2C tenant represents a collection of identities to be used with relying party applications. To
learn more, see Azure AD B2C: Frequently asked questions (FAQ).
Since web APIs have no user interface, they're unable to redirect the user to a secure token service like Azure AD
B2C. Instead, the API is passed a bearer token from the calling app, which has already authenticated the user with
Azure AD B2C. The API then validates the token without direct user interaction.
In this tutorial, learn how to:
Create an Azure Active Directory B2C tenant.
Register a Web API in Azure AD B2C.
Use Visual Studio to create a Web API configured to use the Azure AD B2C tenant for authentication.
Configure policies controlling the behavior of the Azure AD B2C tenant.
Use Postman to simulate a web app which presents a login dialog, retrieves a token, and uses it to make a
request against the web API.
Prerequisites
The following are required for this walkthrough:
Microsoft Azure subscription
Visual Studio 2017 (any edition)
Postman
After the API is registered, the list of apps and APIs in the tenant is displayed. Select the API that was just
registered. Select the Copy icon to the right of the Application ID field to copy it to the clipboard. Select
Published scopes and verify the default user_impersonation scope is present.
SETTING VALUE
Select OK to close the Change Authentication dialog. Select OK to create the web app.
Visual Studio creates the web API with a controller named ValuesController.cs that returns hard-coded values for
GET requests. The class is decorated with the Authorize attribute, so all requests require authentication.
Run the web API
In Visual Studio, run the API. Visual Studio launches a browser pointed at the API's root URL. Note the URL in the
address bar, and leave the API running in the background.
NOTE
Since there is no controller defined for the root URL, the browser may display a 404 (page not found) error. This is expected
behavior.
Name Postman
The newly registered web app needs permission to access the web API on the user's behalf.
1. Select Postman in the list of apps and then select API access from the menu on the left.
2. Select + Add.
3. In the Select API dropdown, select the name of the web API.
4. In the Select Scopes dropdown, ensure all scopes are selected.
5. Select Ok.
Note the Postman app's Application ID, as it's required to obtain a bearer token.
Create a Postman request
Launch Postman. By default, Postman displays the Create New dialog upon launching. If the dialog isn't
displayed, select the + New button in the upper left.
From the Create New dialog:
1. Select Request.
2. Enter Get Values in the Request name box.
3. Select + Create Collection to create a new collection for storing the request. Name the collection ASP.NET
Core tutorials and then select the checkmark.
4. Select the Save to ASP.NET Core tutorials button.
Test the web API without authentication
To verify that the web API requires authentication, first make a request without authentication.
1. In the Enter request URL box, enter the URL for ValuesController . The URL is the same as displayed in
the browser with api/values appended. An example would be https://localhost:44375/api/values .
2. Select the Send button.
3. Note the status of the response is 401 Unauthorized.
IMPORTANT
If you get a "Could not get any response" error, you may need to disable SSL certificate verification in the Postman settings.
Next steps
In this tutorial, you learned how to:
Create an Azure Active Directory B2C tenant.
Register a Web API in Azure AD B2C.
Use Visual Studio to create a Web API configured to use the Azure AD B2C tenant for authentication.
Configure policies controlling the behavior of the Azure AD B2C tenant.
Use Postman to simulate a web app which presents a login dialog, retrieves a token, and uses it to make a
request against the web API.
Continue developing your API by learning to:
Secure an ASP.NET Core web app using Azure AD B2C.
Call a .NET web API from a .NET web app using Azure AD B2C.
Customize the Azure AD B2C user interface.
Configure password complexity requirements.
Enable multi-factor authentication.
Configure additional identity providers, such as Microsoft, Facebook, Google, Amazon, Twitter, and others.
Use the Azure AD Graph API to retrieve additional user information, such as group membership, from the
Azure AD B2C tenant.
Articles based on ASP.NET Core projects created with
individual user accounts
9/21/2018 • 2 minutes to read • Edit Online
ASP.NET Core Identity is included in project templates in Visual Studio with the "Individual User Accounts" option.
The authentication templates are available in .NET Core CLI with -au Individual :
No Authentication
Authentication is specified in the .NET Core CLI with the -au option. In Visual Studio, the Change
Authentication dialog is available for new web applications. The default for new web apps in Visual Studio is No
Authentication.
Projects created with no authentication:
Don't contain web pages and UI to sign in and sign out.
Don't contain authentication code.
Windows Authentication
Windows Authentication is specified for new web apps in the .NET Core CLI with the -au Windows option. In
Visual Studio, the Change Authentication dialog provides the Windows Authentication options.
If Windows Authentication is selected, the app is configured to use the Windows Authentication IIS module.
Windows Authentication is intended for Intranet web sites.
Additional resources
The following articles show how to use the code generated in ASP.NET Core templates that use individual user
accounts:
Two-factor authentication with SMS
Account confirmation and password recovery in ASP.NET Core
Create an ASP.NET Core app with user data protected by authorization
Authorization in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Introduction
Create an app with user data protected by authorization
Razor Pages authorization
Simple authorization
Role-based authorization
Claims-based authorization
Policy-based authorization
Authorization policy providers
Dependency injection in requirement handlers
Resource-based authorization
View -based authorization
Authorize with a specific scheme
Introduction to authorization in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Authorization refers to the process that determines what a user is able to do. For example, an administrative user
is allowed to create a document library, add documents, edit documents, and delete them. A non-administrative
user working with the library is only authorized to read the documents.
Authorization is orthogonal and independent from authentication. However, authorization requires an
authentication mechanism. Authentication is the process of ascertaining who a user is. Authentication may create
one or more identities for the current user.
Authorization types
ASP.NET Core authorization provides a simple, declarative role and a rich policy-based model. Authorization is
expressed in requirements, and handlers evaluate a user's claims against requirements. Imperative checks can be
based on simple policies or policies which evaluate both the user identity and properties of the resource that the
user is attempting to access.
Namespaces
Authorization components, including the AuthorizeAttribute and AllowAnonymousAttribute attributes, are found in
the Microsoft.AspNetCore.Authorization namespace.
Consult the documentation on simple authorization.
9/18/2018 • 18 minutes to read • Edit Online
See this PDF for the ASP.NET Core MVC version. The ASP.NET Core 1.1 version of this tutorial is in this folder.
The 1.1 ASP.NET Core sample is in the samples.
See this pdf
The Approve and Reject buttons are only displayed for managers and administrators.
In the following image, [email protected] is signed in and in the administrators role:
The administrator has all privileges. She can read/edit/delete any contact and change the status of contacts.
The app was created by scaffolding the following Contact model:
Prerequisites
This tutorial is advanced. You should be familiar with:
ASP.NET Core
Authentication
Account Confirmation and Password Recovery
Authorization
Entity Framework Core
In ASP.NET Core 2.1, User.IsInRole fails when using AddDefaultIdentity . This tutorial uses AddDefaultIdentity
and therefore requires ASP.NET Core 2.2 preview 1 or later. See this GitHub issue for a work-around.
OwnerID is the user's ID from the AspNetUser table in the Identity database. The Status field determines if a
contact is viewable by general users.
Create a new migration and update the database:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
You can opt out of authentication at the Razor Page, controller, or action method level with the [AllowAnonymous]
attribute. Setting the default authentication policy to require users to be authenticated protects newly added
Razor Pages and controllers. Having authentication required by default is more secure than relying on new
controllers and Razor Pages to include the [Authorize] attribute.
Add AllowAnonymous to the Index, About, and Contact pages so anonymous users can get information about the
site before they register.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages
{
[AllowAnonymous]
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}
host.Run();
}
// allowed user can create and edit contacts that they create
var managerID = await EnsureUser(serviceProvider, testUserPw, "[email protected]");
await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);
SeedDB(context, adminID);
}
}
return user.Id;
}
if (roleManager == null)
{
throw new Exception("roleManager null");
}
if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}
return IR;
}
Add the administrator user ID and ContactStatus to the contacts. Make one of the contacts "Submitted" and one
"Rejected". Add the user ID and status to all the contacts. Only one contact is shown:
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "[email protected]",
Status = ContactStatus.Approved,
OwnerID = adminID
},
namespace ContactManager.Authorization
{
public class ContactIsOwnerAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
UserManager<IdentityUser> _userManager;
public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser>
userManager)
{
_userManager = userManager;
}
if (resource.OwnerID == _userManager.GetUserId(context.User))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
The ContactIsOwnerAuthorizationHandler calls context.Succeed if the current authenticated user is the contact
owner. Authorization handlers generally:
Return context.Succeed when the requirements are met.
Return Task.CompletedTask when requirements aren't met. Task.CompletedTask is neither success or failure—it
allows other authorization handlers to run.
If you need to explicitly fail, return context.Fail.
The app allows contact owners to edit/delete/create their own data. ContactIsOwnerAuthorizationHandler doesn't
need to check the operation passed in the requirement parameter.
Create a manager authorization handler
Create a ContactManagerAuthorizationHandler class in the Authorization folder. The
ContactManagerAuthorizationHandler verifies the user acting on the resource is a manager. Only managers can
approve or reject content changes (new or changed).
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
namespace ContactManager.Authorization
{
public class ContactManagerAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
return Task.CompletedTask;
}
}
}
namespace ContactManager.Authorization
{
public class ContactAdministratorsAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null)
{
return Task.CompletedTask;
}
return Task.CompletedTask;
}
}
}
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// Authorization handlers.
services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
}
Support authorization
In this section, you update the Razor Pages and add an operations requirements class.
Review the contact operations requirements class
Review the ContactOperations class. This class contains the requirements the app supports:
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public static class ContactOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName};
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
public static OperationAuthorizationRequirement Approve =
new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
public static OperationAuthorizationRequirement Reject =
new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
}
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages.Contacts
{
public class DI_BasePageModel : PageModel
{
protected ApplicationDbContext Context { get; }
protected IAuthorizationService AuthorizationService { get; }
protected UserManager<IdentityUser> UserManager { get; }
public DI_BasePageModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager) : base()
{
Context = context;
UserManager = userManager;
AuthorizationService = authorizationService;
}
}
}
The preceding code:
Adds the IAuthorizationService service to access to the authorization handlers.
Adds the Identity UserManager service.
Add the ApplicationDbContext .
Update the CreateModel
Update the create page model constructor to use the DI_BasePageModel base class:
Contact.OwnerID = UserManager.GetUserId(User);
Context.Contact.Add(Contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// Only approved contacts are shown UNLESS you're authorized to see them
// or you are the owner.
if (!isAuthorized)
{
contacts = contacts.Where(c => c.Status == ContactStatus.Approved
|| c.OwnerID == currentUserId);
}
[BindProperty]
public Contact Contact { get; set; }
if (Contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return new ChallengeResult();
}
return Page();
}
if (contact == null)
{
return NotFound();
}
Contact.OwnerID = contact.OwnerID;
Context.Attach(Contact).State = EntityState.Modified;
if (contact.Status == ContactStatus.Approved)
{
// If the contact is updated after approval,
// and the user cannot approve,
// set the status back to submitted so the update can be
// checked and approved.
var canApprove = await AuthorizationService.AuthorizeAsync(User,
contact,
ContactOperations.Approve);
if (!canApprove.Succeeded)
{
contact.Status = ContactStatus.Submitted;
}
}
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
[BindProperty]
public Contact Contact { get; set; }
if (Contact == null)
{
return NotFound();
}
return Page();
}
if (contact == null)
{
return NotFound();
}
Context.Contact.Remove(Contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Inject the authorization service into the views
Currently, the UI shows edit and delete links for contacts the user can't modify.
Inject the authorization service in the Views/_ViewImports.cshtml file so it's available to all views:
@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService
@page
@model ContactManager.Pages.Contacts.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Address)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].City)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].State)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Zip)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Email)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Status)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Contact)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.City)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.Zip)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Status)
</td>
<td>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
<text> | </text>
}
WARNING
Hiding links from users that don't have permission to change data doesn't secure the app. Hiding links makes the app more
user-friendly by displaying only valid links. Users can hack the generated URLs to invoke edit and delete operations on data
they don't own. The Razor Page or controller must enforce access checks to secure the data.
Update Details
Update the details view so managers can approve or reject contacts:
@*Precedng markup omitted for brevity.*@
<dt>
@Html.DisplayNameFor(model => model.Contact.Email)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Email)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Contact.Status)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Status)
</dd>
</dl>
</div>
<div>
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Index">Back to List</a>
</div>
if (Contact == null)
{
return NotFound();
}
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return new ChallengeResult();
}
return Page();
}
if (contact == null)
{
return NotFound();
}
return RedirectToPage("./Index");
}
}
USER OPTIONS
Create a contact in the administrator's browser. Copy the URL for delete and edit from the administrator contact.
Paste these links into the test user's browser to verify the test user can't perform these operations.
Add Models\Contact.cs:
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
SeedData.Initialize(services, "not used");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
Test that the app seeded the database. If there are any rows in the contact DB, the seed method doesn't run.
Additional resources
ASP.NET Core Authorization Lab. This lab goes into more detail on the security features introduced in this
tutorial.
Authorization in ASP.NET Core: Simple, role, claims-based, and custom
Custom policy-based authorization
Razor Pages authorization conventions in ASP.NET
Core
8/15/2018 • 3 minutes to read • Edit Online
By Luke Latham
One way to control access in your Razor Pages app is to use authorization conventions at startup. These
conventions allow you to authorize users and allow anonymous users to access individual pages or folders of
pages. The conventions described in this topic automatically apply authorization filters to control access.
View or download sample code (how to download)
The sample app uses Cookie authentication without ASP.NET Core Identity. The user account for the hypothetical
user, Maria Rodriguez, is hardcoded into the app. Use the Email username "[email protected]" and
any password to sign in the user. The user is authenticated in the AuthenticateUser method in the
Pages/Account/Login.cshtml.cs file. In a real-world example, the user would be authenticated against a database.
To use ASP.NET Core Identity, follow the guidance in the Introduction to Identity on ASP.NET Core topic. The
concepts and examples shown in this topic apply equally to apps that use ASP.NET Core Identity.
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
The specified path is the View Engine path, which is the Razor Pages root relative path without an extension and
containing only forward slashes.
An AuthorizePage overload is available if you need to specify an authorization policy.
NOTE
An AuthorizeFilter can be applied to a page model class with the [Authorize] filter attribute. For more information,
see Authorize filter attribute.
The specified path is the View Engine path, which is the Razor Pages root relative path.
An AuthorizeFolder overload is available if you need to specify an authorization policy.
options.Conventions.AuthorizeAreaPage("Identity", "/Manage/Accounts");
The page name is the path of the file without an extension relative to the pages root directory for the specified
area. For example, the page name for the file Areas/Identity/Pages/Manage/Accounts.cshtml is
/Manage/Accounts.
An AuthorizeAreaPage overload is available if you need to specify an authorization policy.
options.Conventions.AuthorizeAreaFolder("Identity", "/Manage");
The folder path is the path of the folder relative to the pages root directory for the specified area. For example, the
folder path for the files under Areas/Identity/Pages/Manage/ is /Manage.
An AuthorizeAreaFolder overload is available if you need to specify an authorization policy.
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
The specified path is the View Engine path, which is the Razor Pages root relative path without an extension and
containing only forward slashes.
Allow anonymous access to a folder of pages
Use the AllowAnonymousToFolder convention via AddRazorPagesOptions to add an AllowAnonymousFilter to
all of the pages in a folder at the specified path:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
The specified path is the View Engine path, which is the Razor Pages root relative path.
// This works.
.AuthorizeFolder("/Private").AllowAnonymousToPage("/Private/Public")
The reverse, however, isn't true. You can't declare a folder of pages for anonymous access and specify a page
within for authorization:
Requiring authorization on the Private page won't work because when both the AllowAnonymousFilter and
AuthorizeFilter filters are applied to the page, the AllowAnonymousFilter wins and controls access.
Additional resources
Razor Pages custom route and page model providers
PageConventionCollection class
Simple authorization in ASP.NET Core
6/26/2018 • 2 minutes to read • Edit Online
Authorization in MVC is controlled through the AuthorizeAttribute attribute and its various parameters. At its
simplest, applying the AuthorizeAttribute attribute to a controller or action limits access to the controller or
action to any authenticated user.
For example, the following code limits access to the AccountController to any authenticated user.
[Authorize]
public class AccountController : Controller
{
public ActionResult Login()
{
}
If you want to apply authorization to an action rather than the controller, apply the AuthorizeAttribute attribute
to the action itself:
[Authorize]
public ActionResult Logout()
{
}
}
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
This would allow only authenticated users to the AccountController , except for the Login action, which is
accessible by everyone, regardless of their authenticated or unauthenticated / anonymous status.
WARNING
[AllowAnonymous] bypasses all authorization statements. If you combine [AllowAnonymous] and any [Authorize]
attribute, the [Authorize] attributes are ignored. For example if you apply [AllowAnonymous] at the controller level,
any [Authorize] attributes on the same controller (or on any action within it) is ignored.
Role-based authorization in ASP.NET Core
7/30/2018 • 2 minutes to read • Edit Online
When an identity is created it may belong to one or more roles. For example, Tracy may belong to the
Administrator and User roles whilst Scott may only belong to the User role. How these roles are created and
managed depends on the backing store of the authorization process. Roles are exposed to the developer through
the IsInRole method on the ClaimsPrincipal class.
IMPORTANT
This topic does not apply to Razor Pages. Razor Pages supports IPageFilter and IAsyncPageFilter. For more information, see
Filter methods for Razor Pages.
[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
}
[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}
This controller would be only accessible by users who are members of the HRManager role or the Finance role.
If you apply multiple attributes then an accessing user must be a member of all the roles specified; the following
sample requires that a user must be a member of both the PowerUser and ControlPanelUser role.
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
public class ControlPanelController : Controller
{
}
You can further limit access by applying additional role authorization attributes at the action level:
[Authorize(Roles = "Administrator, PowerUser")]
public class ControlPanelController : Controller
{
public ActionResult SetTime()
{
}
[Authorize(Roles = "Administrator")]
public ActionResult ShutDown()
{
}
}
In the previous code snippet members of the Administrator role or the PowerUser role can access the controller
and the SetTime action, but only members of the Administrator role can access the ShutDown action.
You can also lock down a controller but allow anonymous, unauthenticated access to individual actions.
[Authorize]
public class ControlPanelController : Controller
{
public ActionResult SetTime()
{
}
[AllowAnonymous]
public ActionResult Login()
{
}
}
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Administrator"));
});
}
Policies are applied using the Policy property on the AuthorizeAttribute attribute:
[Authorize(Policy = "RequireAdministratorRole")]
public IActionResult Shutdown()
{
return View();
}
If you want to specify multiple allowed roles in a requirement then you can specify them as parameters to the
RequireRole method:
options.AddPolicy("ElevatedRights", policy =>
policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
This example authorizes users who belong to the Administrator , PowerUser or BackupAdministrator roles.
Claims-based authorization in ASP.NET Core
6/21/2018 • 3 minutes to read • Edit Online
When an identity is created it may be assigned one or more claims issued by a trusted party. A claim is a name
value pair that represents what the subject is, not what the subject can do. For example, you may have a driver's
license, issued by a local driving license authority. Your driver's license has your date of birth on it. In this case the
claim name would be DateOfBirth , the claim value would be your date of birth, for example 8th June 1970 and
the issuer would be the driving license authority. Claims based authorization, at its simplest, checks the value of a
claim and allows access to a resource based upon that value. For example if you want access to a night club the
authorization process might be:
The door security officer would evaluate the value of your date of birth claim and whether they trust the issuer
(the driving license authority) before granting you access.
An identity can contain multiple claims with multiple values and can contain multiple claims of the same type.
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}
In this case the EmployeeOnly policy checks for the presence of an EmployeeNumber claim on the current identity.
You then apply the policy using the Policy property on the AuthorizeAttribute attribute to specify the policy
name;
[Authorize(Policy = "EmployeeOnly")]
public IActionResult VacationBalance()
{
return View();
}
The AuthorizeAttribute attribute can be applied to an entire controller, in this instance only identities matching
the policy will be allowed access to any Action on the controller.
[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
public ActionResult VacationBalance()
{
}
}
If you have a controller that's protected by the AuthorizeAttribute attribute, but want to allow anonymous access
to particular actions you apply the AllowAnonymousAttribute attribute.
[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
public ActionResult VacationBalance()
{
}
[AllowAnonymous]
public ActionResult VacationPolicy()
{
}
}
Most claims come with a value. You can specify a list of allowed values when creating the policy. The following
example would only succeed for employees whose employee number was 1, 2, 3, 4 or 5.
services.AddAuthorization(options =>
{
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
}
[Authorize(Policy = "HumanResources")]
public ActionResult UpdateSalary()
{
}
}
In the above example any identity which fulfills the EmployeeOnly policy can access the Payslip action as that
policy is enforced on the controller. However in order to call the UpdateSalary action the identity must fulfill both
the EmployeeOnly policy and the HumanResources policy.
If you want more complicated policies, such as taking a date of birth claim, calculating an age from it then
checking the age is 21 or older then you need to write custom policy handlers.
Policy-based authorization in ASP.NET Core
6/21/2018 • 6 minutes to read • Edit Online
Underneath the covers, role-based authorization and claims-based authorization use a requirement, a
requirement handler, and a pre-configured policy. These building blocks support the expression of authorization
evaluations in code. The result is a richer, reusable, testable authorization structure.
An authorization policy consists of one or more requirements. It's registered as part of the authorization service
configuration, in the Startup.ConfigureServices method:
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
}
In the preceding example, an "AtLeast21" policy is created. It has a single requirement—that of a minimum age,
which is supplied as a parameter to the requirement.
Policies are applied by using the [Authorize] attribute with the policy name. For example:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
public IActionResult Login() => View();
Requirements
An authorization requirement is a collection of data parameters that a policy can use to evaluate the current user
principal. In our "AtLeast21" policy, the requirement is a single parameter—the minimum age. A requirement
implements IAuthorizationRequirement, which is an empty marker interface. A parameterized minimum age
requirement could be implemented as follows:
using Microsoft.AspNetCore.Authorization;
NOTE
A requirement doesn't need to have data or properties.
Authorization handlers
An authorization handler is responsible for the evaluation of a requirement's properties. The authorization
handler evaluates the requirements against a provided AuthorizationHandlerContext to determine if access is
allowed.
A requirement can have multiple handlers. A handler may inherit AuthorizationHandler<TRequirement>, where
TRequirement is the requirement to be handled. Alternatively, a handler may implement IAuthorizationHandler
to handle more than one type of requirement.
Use a handler for one requirement
The following is an example of a one-to-one relationship in which a minimum age handler utilizes a single
requirement:
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
The preceding code determines if the current user principal has a date of birth claim which has been issued by a
known and trusted Issuer. Authorization can't occur when the claim is missing, in which case a completed task is
returned. When a claim is present, the user's age is calculated. If the user meets the minimum age defined by the
requirement, authorization is deemed successful. When authorization is successful, context.Succeed is invoked
with the satisfied requirement as its sole parameter.
Use a handler for multiple requirements
The following is an example of a one-to-many relationship in which a permission handler utilizes three
requirements:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
return true;
}
return true;
}
}
The preceding code traverses PendingRequirements—a property containing requirements not marked as
successful. If the user has read permission, he or she must be either an owner or a sponsor to access the
requested resource. If the user has edit or delete permission, he or she must be an owner to access the requested
resource. When authorization is successful, context.Succeed is invoked with the satisfied requirement as its sole
parameter.
Handler registration
Handlers are registered in the services collection during configuration. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}
When set to false , the InvokeHandlersAfterFailure property (available in ASP.NET Core 1.1 and later) short-
circuits the execution of handlers when context.Fail is called. InvokeHandlersAfterFailure defaults to true , in
which case all handlers are called. This allows requirements to produce side effects, such as logging, which
always take place even if context.Fail has been called in another handler.
using Microsoft.AspNetCore.Authorization;
BadgeEntryHandler.cs
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
using System.Security.Claims;
using System.Threading.Tasks;
TemporaryStickerHandler.cs
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
using System.Security.Claims;
using System.Threading.Tasks;
Ensure that both handlers are registered. If either handler succeeds when a policy evaluates the
BuildingEntryRequirement , the policy evaluation succeeds.
By Mike Rousos
Typically when using policy-based authorization, policies are registered by calling AuthorizationOptions.AddPolicy
as part of authorization service configuration. In some scenarios, it may not be possible (or desirable) to register all
authorization policies in this way. In those cases, you can use a custom IAuthorizationPolicyProvider to control
how authorization policies are supplied.
Examples of scenarios where a custom IAuthorizationPolicyProvider may be useful include:
Using an external service to provide policy evaluation.
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to
add each individual authorization policy with an AuthorizationOptions.AddPolicy call.
Creating policies at runtime based on information in an external data source (like a database) or determining
authorization requirements dynamically through another mechanism.
View or download sample code from the aspnet/AuthSamples GitHub repository. Download the
aspnet/AuthSamples repository ZIP file. Unzip the AuthSamples-master.zip file. Navigate to the
samples/CustomPolicyProvider project folder.
You can customize this behavior by registering a different IAuthorizationPolicyProvider implementation in the
app’s dependency injection container.
The IAuthorizationPolicyProvider interface contains two APIs:
GetPolicyAsync returns an authorization policy for a given name.
GetDefaultPolicyAsync returns the default authorization policy (the policy used for [Authorize] attributes
without a policy specified).
By implementing these two APIs, you can customize how authorization policies are provided.
// Get or set the Age property by manipulating the underlying Policy property
public int Age
{
get
{
if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age))
{
return age;
}
return default(int);
}
set
{
Policy = $"{POLICY_PREFIX}{value.ToString()}";
}
}
}
This attribute type has a Policy string based on the hard-coded prefix ( "MinimumAge" ) and an integer passed in via
the constructor.
You can apply it to actions in the same way as other Authorize attributes except that it takes an integer as a
parameter.
[MinimumAgeAuthorize(10)]
public IActionResult RequiresMinimumAge10()
Custom IAuthorizationPolicyProvider
The custom MinimumAgeAuthorizeAttribute makes it easy to request authorization policies for any minimum age
desired. The next problem to solve is making sure that authorization policies are available for all of those different
ages. This is where an IAuthorizationPolicyProvider is useful.
When using MinimumAgeAuthorizationAttribute , the authorization policy names will follow the pattern
"MinimumAge" + Age , so the custom IAuthorizationPolicyProvider should generate authorization policies by:
return Task.FromResult<AuthorizationPolicy>(null);
}
}
Default policy
In addition to providing named authorization policies, a custom IAuthorizationPolicyProvider needs to implement
GetDefaultPolicyAsync to provide an authorization policy for [Authorize] attributes without a policy name
specified.
In many cases, this authorization attribute only requires an authenticated user, so you can make the necessary
policy with a call to RequireAuthenticatedUser :
As with all aspects of a custom IAuthorizationPolicyProvider , you can customize this, as needed. In some cases:
Default authorization policies might not be used.
Retrieving the default policy can be delegated to a fallback IAuthorizationPolicyProvider .
services.AddTransient<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
Authorization handlers must be registered in the service collection during configuration (using dependency
injection).
Suppose you had a repository of rules you wanted to evaluate inside an authorization handler and that repository
was registered in the service collection. Authorization will resolve and inject that into your constructor.
For example, if you wanted to use ASP.NET's logging infrastructure you would want to inject ILoggerFactory into
your handler. Such a handler might look like:
services.AddSingleton<IAuthorizationHandler, LoggingAuthorizationHandler>();
An instance of the handler will be created when your application starts, and DI will inject the registered
ILoggerFactory into your constructor.
NOTE
Handlers that use Entity Framework shouldn't be registered as singletons.
Resource-based authorization in ASP.NET Core
7/30/2018 • 5 minutes to read • Edit Online
Authorization strategy depends upon the resource being accessed. Consider a document which has an author
property. Only the author is allowed to update the document. Consequently, the document must be retrieved from
the data store before authorization evaluation can occur.
Attribute evaluation occurs before data binding and before execution of the page handler or action which loads the
document. For these reasons, declarative authorization with an [Authorize] attribute won't suffice. Instead, you
can invoke a custom authorization method—a style known as imperative authorization.
Use the sample apps (how to download) to explore the features described in this topic.
Create an ASP.NET Core app with user data protected by authorization contains a sample app that uses resource-
based authorization.
IAuthorizationService has two AuthorizeAsync method overloads: one accepting the resource and the policy
name and the other accepting the resource and a list of requirements to evaluate.
ASP.NET Core 2.x
ASP.NET Core 1.x
In the following example, the resource to be secured is loaded into a custom Document object. An AuthorizeAsync
overload is invoked to determine whether the current user is allowed to edit the provided document. A custom
"EditPolicy" authorization policy is factored into the decision. See Custom policy-based authorization for more on
creating authorization policies.
NOTE
The following code samples assume authentication has run and set the User property.
if (Document == null)
{
return new NotFoundResult();
}
if (authorizationResult.Succeeded)
{
return Page();
}
else if (User.Identity.IsAuthenticated)
{
return new ForbidResult();
}
else
{
return new ChallengeResult();
}
}
return Task.CompletedTask;
}
}
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("EditPolicy", policy =>
policy.Requirements.Add(new SameAuthorRequirement()));
});
services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationCrudHandler>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
Operational requirements
If you're making decisions based on the outcomes of CRUD (Create, Read, Update, Delete) operations, use the
OperationAuthorizationRequirement helper class. This class enables you to write a single handler instead of an
individual class for each operation type. To use it, provide some operation names:
return Task.CompletedTask;
}
}
The preceding handler validates the operation using the resource, the user's identity, and the requirement's Name
property.
To call an operational resource handler, specify the operation when invoking AuthorizeAsync in your page handler
or action. The following example determines whether the authenticated user is permitted to view the provided
document.
NOTE
The following code samples assume authentication has run and set the User property.
if (Document == null)
{
return new NotFoundResult();
}
if (authorizationResult.Succeeded)
{
return Page();
}
else if (User.Identity.IsAuthenticated)
{
return new ForbidResult();
}
else
{
return new ChallengeResult();
}
}
If authorization succeeds, the page for viewing the document is returned. If authorization fails but the user is
authenticated, returning ForbidResult informs any authentication middleware that authorization failed. A
ChallengeResult is returned when authentication must be performed. For interactive browser clients, it may be
appropriate to redirect the user to a login page.
View-based authorization in ASP.NET Core MVC
7/30/2018 • 2 minutes to read • Edit Online
A developer often wants to show, hide, or otherwise modify a UI based on the current user identity. You can access
the authorization service within MVC views via dependency injection. To inject the authorization service into a
Razor view, use the @inject directive:
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
If you want the authorization service in every view, place the @inject directive into the _ViewImports.cshtml file of
the Views directory. For more information, see Dependency injection into views.
Use the injected authorization service to invoke AuthorizeAsync in exactly the same way you would check during
resource-based authorization:
ASP.NET Core 2.x
ASP.NET Core 1.x
In some cases, the resource will be your view model. Invoke AuthorizeAsync in exactly the same way you would
check during resource-based authorization:
ASP.NET Core 2.x
ASP.NET Core 1.x
In the preceding code, the model is passed as a resource the policy evaluation should take into consideration.
WARNING
Don't rely on toggling visibility of your app's UI elements as the sole authorization check. Hiding a UI element may not
completely prevent access to its associated controller action. For example, consider the button in the preceding code snippet.
A user can invoke the Edit action method if he or she knows the relative resource URL is /Document/Edit/1. For this
reason, the Edit action method should perform its own authorization check.
Authorize with a specific scheme in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
In some scenarios, such as Single Page Applications (SPAs), it's common to use multiple authentication methods.
For example, the app may use cookie-based authentication to log in and JWT bearer authentication for JavaScript
requests. In some cases, the app may have multiple instances of an authentication handler. For example, two
cookie handlers where one contains a basic identity and one is created when a multi-factor authentication (MFA)
has been triggered. MFA may be triggered because the user requested an operation that requires extra security.
ASP.NET Core 2.x
ASP.NET Core 1.x
An authentication scheme is named when the authentication service is configured during authentication. For
example:
services.AddAuthentication()
.AddCookie(options => {
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(options => {
options.Audience = "http://localhost:5001/";
options.Authority = "http://localhost:5000/";
});
In the preceding code, two authentication handlers have been added: one for cookies and one for bearer.
NOTE
Specifying the default scheme results in the HttpContext.User property being set to that identity. If that behavior isn't
desired, disable it by invoking the parameterless form of AddAuthentication .
In the preceding example, both the cookie and bearer handlers run and have a chance to create and append an
identity for the current user. By specifying a single scheme only, the corresponding handler runs.
ASP.NET Core 2.x
ASP.NET Core 1.x
[Authorize(AuthenticationSchemes =
JwtBearerDefaults.AuthenticationScheme)]
public class MixedController : Controller
In the preceding code, only the handler with the "Bearer" scheme runs. Any cookie-based identities are ignored.
services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new MinimumAgeRequirement());
});
});
In the preceding example, the "Over18" policy only runs against the identity created by the "Bearer" handler. Use
the policy by setting the [Authorize] attribute's Policy property:
[Authorize(Policy = "Over18")]
public class RegistrationController : Controller
Data Protection in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Web applications often need to store security-sensitive data. Windows provides DPAPI for desktop applications
but this is unsuitable for web applications. The ASP.NET Core data protection stack provide a simple, easy to use
cryptographic API a developer can use to protect data, including key management and rotation.
The ASP.NET Core data protection stack is designed to serve as the long-term replacement for the
<machineKey> element in ASP.NET 1.x - 4.x. It was designed to address many of the shortcomings of the old
cryptographic stack while providing an out-of-the-box solution for the majority of use cases modern applications
are likely to encounter.
Problem statement
The overall problem statement can be succinctly stated in a single sentence: I need to persist trusted information
for later retrieval, but I don't trust the persistence mechanism. In web terms, this might be written as "I need to
round-trip trusted state via an untrusted client."
The canonical example of this is an authentication cookie or bearer token. The server generates an "I am Groot
and have xyz permissions" token and hands it to the client. At some future date the client will present that token
back to the server, but the server needs some kind of assurance that the client hasn't forged the token. Thus the
first requirement: authenticity (a.k.a. integrity, tamper-proofing).
Since the persisted state is trusted by the server, we anticipate that this state might contain information that's
specific to the operating environment. This could be in the form of a file path, a permission, a handle or other
indirect reference, or some other piece of server-specific data. Such information should generally not be disclosed
to an untrusted client. Thus the second requirement: confidentiality.
Finally, since modern applications are componentized, what we've seen is that individual components will want to
take advantage of this system without regard to other components in the system. For instance, if a bearer token
component is using this stack, it should operate without interference from an anti-CSRF mechanism that might
also be using the same stack. Thus the final requirement: isolation.
We can provide further constraints in order to narrow the scope of our requirements. We assume that all services
operating within the cryptosystem are equally trusted and that the data doesn't need to be generated or
consumed outside of the services under our direct control. Furthermore, we require that operations are as fast as
possible since each request to the web service might go through the cryptosystem one or more times. This makes
symmetric cryptography ideal for our scenario, and we can discount asymmetric cryptography until such a time
that it's needed.
Design philosophy
We started by identifying problems with the existing stack. Once we had that, we surveyed the landscape of
existing solutions and concluded that no existing solution quite had the capabilities we sought. We then
engineered a solution based on several guiding principles.
The system should offer simplicity of configuration. Ideally the system would be zero-configuration and
developers could hit the ground running. In situations where developers need to configure a specific aspect
(such as the key repository), consideration should be given to making those specific configurations simple.
Offer a simple consumer-facing API. The APIs should be easy to use correctly and difficult to use
incorrectly.
Developers shouldn't learn key management principles. The system should handle algorithm selection and
key lifetime on the developer's behalf. Ideally the developer should never even have access to the raw key
material.
Keys should be protected at rest when possible. The system should figure out an appropriate default
protection mechanism and apply it automatically.
With these principles in mind we developed a simple, easy to use data protection stack.
The ASP.NET Core data protection APIs are not primarily intended for indefinite persistence of confidential
payloads. Other technologies like Windows CNG DPAPI and Azure Rights Management are more suited to the
scenario of indefinite storage, and they have correspondingly strong key management capabilities. That said,
there's nothing prohibiting a developer from using the ASP.NET Core data protection APIs for long-term
protection of confidential data.
Audience
The data protection system is divided into five main packages. Various aspects of these APIs target three main
audiences;
1. The Consumer APIs Overview target application and framework developers.
"I don't want to learn about how the stack operates or about how it's configured. I simply want to perform
some operation in as simple a manner as possible with high probability of using the APIs successfully."
2. The configuration APIs target application developers and system administrators.
"I need to tell the data protection system that my environment requires non-default paths or settings."
3. The extensibility APIs target developers in charge of implementing custom policy. Usage of these APIs
would be limited to rare situations and experienced, security aware developers.
"I need to replace an entire component within the system because I have truly unique behavioral
requirements. I am willing to learn uncommonly-used parts of the API surface in order to build a plugin
that fulfills my requirements."
Package layout
The data protection stack consists of five packages.
Microsoft.AspNetCore.DataProtection.Abstractions contains the IDataProtectionProvider and
IDataProtector interfaces to create data protection services. It also contains useful extension methods for
working with these types (for example, IDataProtector.Protect). If the data protection system is instantiated
elsewhere and you're consuming the API, reference Microsoft.AspNetCore.DataProtection.Abstractions .
Microsoft.AspNetCore.DataProtection contains the core implementation of the data protection system,
including core cryptographic operations, key management, configuration, and extensibility. To instantiate
the data protection system (for example, adding it to an IServiceCollection) or modifying or extending its
behavior, reference Microsoft.AspNetCore.DataProtection .
Microsoft.AspNetCore.DataProtection.Extensions contains additional APIs which developers might find
useful but which don't belong in the core package. For instance, this package contains factory methods to
instantiate the data protection system to store keys at a location on the file system without dependency
injection (see DataProtectionProvider). It also contains extension methods for limiting the lifetime of
protected payloads (see ITimeLimitedDataProtector).
Microsoft.AspNetCore.DataProtection.SystemWeb can be installed into an existing ASP.NET 4.x app to
redirect its <machineKey> operations to use the new ASP.NET Core data protection stack. For more
information, see Replace the ASP.NET machineKey in ASP.NET Core.
Microsoft.AspNetCore.Cryptography.KeyDerivation provides an implementation of the PBKDF2 password
hashing routine and can be used by systems that must handle user passwords securely. For more
information, see Hash passwords in ASP.NET Core.
Additional resources
Host ASP.NET Core in a web farm
Get started with the Data Protection APIs in ASP.NET
Core
8/16/2018 • 2 minutes to read • Edit Online
/*
* SAMPLE OUTPUT
*
* Enter input: Hello world!
* Protect returned: CfDJ8ICcgQwZZhlAlTZT...OdfH66i1PnGmpCR5e441xQ
* Unprotect returned: Hello world!
*/
When you create a protector you must provide one or more Purpose Strings. A purpose string provides isolation
between consumers. For example, a protector created with a purpose string of "green" wouldn't be able to
unprotect data provided by a protector with a purpose of "purple".
TIP
Instances of IDataProtectionProvider and IDataProtector are thread-safe for multiple callers. It's intended that once a
component gets a reference to an IDataProtector via a call to CreateProtector , it will use that reference for multiple
calls to Protect and Unprotect .
A call to Unprotect will throw CryptographicException if the protected payload cannot be verified or deciphered. Some
components may wish to ignore errors during unprotect operations; a component which reads authentication cookies might
handle this error and treat the request as if it had no cookie at all rather than fail the request outright. Components which
want this behavior should specifically catch CryptographicException instead of swallowing all exceptions.
Consumer APIs for ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
The IDataProtectionProvider and IDataProtector interfaces are the basic interfaces through which consumers
use the data protection system. They're located in the Microsoft.AspNetCore.DataProtection.Abstractions
package.
IDataProtectionProvider
The provider interface represents the root of the data protection system. It cannot directly be used to protect or
unprotect data. Instead, the consumer must get a reference to an IDataProtector by calling
IDataProtectionProvider.CreateProtector(purpose) , where purpose is a string that describes the intended
consumer use case. See Purpose Strings for much more information on the intent of this parameter and how to
choose an appropriate value.
IDataProtector
The protector interface is returned by a call to CreateProtector , and it's this interface which consumers can use to
perform protect and unprotect operations.
To protect a piece of data, pass the data to the Protect method. The basic interface defines a method which
converts byte[] -> byte[], but there's also an overload (provided as an extension method) which converts string ->
string. The security offered by the two methods is identical; the developer should choose whichever overload is
most convenient for their use case. Irrespective of the overload chosen, the value returned by the Protect method
is now protected (enciphered and tamper-proofed), and the application can send it to an untrusted client.
To unprotect a previously-protected piece of data, pass the protected data to the Unprotect method. (There are
byte[]-based and string-based overloads for developer convenience.) If the protected payload was generated by
an earlier call to Protect on this same IDataProtector , the Unprotect method will return the original
unprotected payload. If the protected payload has been tampered with or was produced by a different
IDataProtector , the Unprotect method will throw CryptographicException.
The concept of same vs. different IDataProtector ties back to the concept of purpose. If two IDataProtector
instances were generated from the same root IDataProtectionProvider but via different purpose strings in the call
to IDataProtectionProvider.CreateProtector , then they're considered different protectors, and one won't be able to
unprotect payloads generated by the other.
NOTE
Some applications (such as console applications or ASP.NET 4.x applications) might not be DI-aware so cannot use the
mechanism described here. For these scenarios consult the Non DI Aware Scenarios document for more information on
getting an instance of an IDataProtection provider without going through DI.
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
/*
* SAMPLE OUTPUT
*
* Enter input: Hello world!
* Protect returned: CfDJ8ICcgQwZZhlAlTZT...OdfH66i1PnGmpCR5e441xQ
* Unprotect returned: Hello world!
*/
TIP
Instances of IDataProtectionProvider and IDataProtector are thread-safe for multiple callers. It's intended that once
a component gets a reference to an IDataProtector via a call to CreateProtector , it will use that reference for multiple
calls to Protect and Unprotect . A call to Unprotect will throw CryptographicException if the protected payload cannot
be verified or deciphered. Some components may wish to ignore errors during unprotect operations; a component which
reads authentication cookies might handle this error and treat the request as if it had no cookie at all rather than fail the
request outright. Components which want this behavior should specifically catch CryptographicException instead of
swallowing all exceptions.
Purpose strings in ASP.NET Core
6/21/2018 • 3 minutes to read • Edit Online
Components which consume IDataProtectionProvider must pass a unique purposes parameter to the
CreateProtector method. The purposes parameter is inherent to the security of the data protection system, as it
provides isolation between cryptographic consumers, even if the root cryptographic keys are the same.
When a consumer specifies a purpose, the purpose string is used along with the root cryptographic keys to
derive cryptographic subkeys unique to that consumer. This isolates the consumer from all other cryptographic
consumers in the application: no other component can read its payloads, and it cannot read any other
component's payloads. This isolation also renders infeasible entire categories of attack against the component.
In the diagram above, IDataProtector instances A and B cannot read each other's payloads, only their own.
The purpose string doesn't have to be secret. It should simply be unique in the sense that no other well-behaved
component will ever provide the same purpose string.
TIP
Using the namespace and type name of the component consuming the data protection APIs is a good rule of thumb, as in
practice this information will never conflict.
A Contoso-authored component which is responsible for minting bearer tokens might use Contoso.Security.BearerToken as
its purpose string. Or - even better - it might use Contoso.Security.BearerToken.v1 as its purpose string. Appending the
version number allows a future version to use Contoso.Security.BearerToken.v2 as its purpose, and the different versions
would be completely isolated from one another as far as payloads go.
Since the purposes parameter to CreateProtectoris a string array, the above could've been instead specified as
[ "Contoso.Security.BearerToken", "v1" ] . This allows establishing a hierarchy of purposes and opens up the
possibility of multi-tenancy scenarios with the data protection system.
WARNING
Components shouldn't allow untrusted user input to be the sole source of input for the purposes chain.
For example, consider a component Contoso.Messaging.SecureMessage which is responsible for storing secure messages. If
the secure messaging component were to call CreateProtector([ username ]) , then a malicious user might create an
account with username "Contoso.Security.BearerToken" in an attempt to get the component to call
CreateProtector([ "Contoso.Security.BearerToken" ]) , thus inadvertently causing the secure messaging system to
mint payloads that could be perceived as authentication tokens.
A better purposes chain for the messaging component would be
CreateProtector([ "Contoso.Messaging.SecureMessage", "User: username" ]) , which provides proper isolation.
The isolation provided by and behaviors of IDataProtectionProvider , IDataProtector , and purposes are as
follows:
For a given IDataProtectionProvider object, the CreateProtector method will create an IDataProtector
object uniquely tied to both the IDataProtectionProvider object which created it and the purposes
parameter which was passed into the method.
The purpose parameter must not be null. (If purposes is specified as an array, this means that the array
must not be of zero length and all elements of the array must be non-null.) An empty string purpose is
technically allowed but is discouraged.
Two purposes arguments are equivalent if and only if they contain the same strings (using an ordinal
comparer) in the same order. A single purpose argument is equivalent to the corresponding single-
element purposes array.
Two IDataProtector objects are equivalent if and only if they're created from equivalent
IDataProtectionProvider objects with equivalent purposes parameters.
For a given IDataProtector object, a call to Unprotect(protectedData) will return the original
unprotectedData if and only if protectedData := Protect(unprotectedData) for an equivalent
IDataProtector object.
NOTE
We're not considering the case where some component intentionally chooses a purpose string which is known to conflict
with another component. Such a component would essentially be considered malicious, and this system isn't intended to
provide security guarantees in the event that malicious code is already running inside of the worker process.
Purpose hierarchy and multi-tenancy in ASP.NET
Core
8/10/2018 • 2 minutes to read • Edit Online
Since an IDataProtector is also implicitly an IDataProtectionProvider , purposes can be chained together. In this
sense, provider.CreateProtector([ "purpose1", "purpose2" ]) is equivalent to
provider.CreateProtector("purpose1").CreateProtector("purpose2") .
This allows for some interesting hierarchical relationships through the data protection system. In the earlier
example of Contoso.Messaging.SecureMessage, the SecureMessage component can call
provider.CreateProtector("Contoso.Messaging.SecureMessage") once up-front and cache the result into a private
_myProvider field. Future protectors can then be created via calls to
_myProvider.CreateProtector("User: username") , and these protectors would be used for securing the individual
messages.
This can also be flipped. Consider a single logical application which hosts multiple tenants (a CMS seems
reasonable), and each tenant can be configured with its own authentication and state management system. The
umbrella application has a single master provider, and it calls provider.CreateProtector("Tenant 1") and
provider.CreateProtector("Tenant 2") to give each tenant its own isolated slice of the data protection system. The
tenants could then derive their own individual protectors based on their own needs, but no matter how hard they
try they cannot create protectors which collide with any other tenant in the system. Graphically, this is represented
as below.
WARNING
This assumes the umbrella application controls which APIs are available to individual tenants and that tenants cannot
execute arbitrary code on the server. If a tenant can execute arbitrary code, they could perform private reflection to break
the isolation guarantees, or they could just read the master keying material directly and derive whatever subkeys they
desire.
The data protection system actually uses a sort of multi-tenancy in its default out-of-the-box configuration. By
default master keying material is stored in the worker process account's user profile folder (or the registry, for IIS
application pool identities). But it's actually fairly common to use a single account to run multiple applications, and
thus all these applications would end up sharing the master keying material. To solve this, the data protection
system automatically inserts a unique-per-application identifier as the first element in the overall purpose chain.
This implicit purpose serves to isolate individual applications from one another by effectively treating each
application as a unique tenant within the system, and the protector creation process looks identical to the image
above.
Hash passwords in ASP.NET Core
9/18/2018 • 2 minutes to read • Edit Online
2. The KeyDerivation.Pbkdf2 method detects the current operating system and attempts to choose the most
optimized implementation of the routine, providing much better performance in certain cases. (On
Windows 8, it offers around 10x the throughput of Rfc2898DeriveBytes .)
3. The KeyDerivation.Pbkdf2 method requires the caller to specify all parameters (salt, PRF, and iteration
count). The Rfc2898DeriveBytes type provides default values for these.
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
/*
* SAMPLE OUTPUT
*
* Enter a password: Xtw9NMgx
* Salt: NZsP6NnmfBuYeJrrAKNuVQ==
* Hashed: /OOoOer10+tGwTRDTrQSoeCxVTFr6dtYly7d0cPxIak=
*/
See the source code for ASP.NET Core Identity's PasswordHasher type for a real-world use case.
Limit the lifetime of protected payloads in ASP.NET
Core
6/21/2018 • 2 minutes to read • Edit Online
There are scenarios where the application developer wants to create a protected payload that expires after a set
period of time. For instance, the protected payload might represent a password reset token that should only be
valid for one hour. It's certainly possible for the developer to create their own payload format that contains an
embedded expiration date, and advanced developers may wish to do this anyway, but for the majority of
developers managing these expirations can grow tedious.
To make this easier for our developer audience, the package Microsoft.AspNetCore.DataProtection.Extensions
contains utility APIs for creating payloads that automatically expire after a set period of time. These APIs hang off
of the ITimeLimitedDataProtector type.
API usage
The ITimeLimitedDataProtector interface is the core interface for protecting and unprotecting time-limited / self-
expiring payloads. To create an instance of an ITimeLimitedDataProtector , you'll first need an instance of a regular
IDataProtector constructed with a specific purpose. Once the IDataProtector instance is available, call the
IDataProtector.ToTimeLimitedDataProtector extension method to get back a protector with built-in expiration
capabilities.
ITimeLimitedDataProtector exposes the following API surface and extension methods:
CreateProtector(string purpose) : ITimeLimitedDataProtector - This API is similar to the existing
IDataProtectionProvider.CreateProtector in that it can be used to create purpose chains from a root time-
limited protector.
Protect(byte[] plaintext, DateTimeOffset expiration) : byte[]
Protect(byte[] plaintext, TimeSpan lifetime) : byte[]
Protect(byte[] plaintext) : byte[]
Protect(string plaintext, DateTimeOffset expiration) : string
Protect(string plaintext, TimeSpan lifetime) : string
Protect(string plaintext) : string
In addition to the core Protect methods which take only the plaintext, there are new overloads which allow
specifying the payload's expiration date. The expiration date can be specified as an absolute date (via a
DateTimeOffset ) or as a relative time (from the current system time, via a TimeSpan ). If an overload which doesn't
take an expiration is called, the payload is assumed never to expire.
Unprotect(byte[] protectedData, out DateTimeOffset expiration) : byte[]
Unprotect(byte[] protectedData) : byte[]
Unprotect(string protectedData, out DateTimeOffset expiration) : string
Unprotect(string protectedData) : string
The Unprotect methods return the original unprotected data. If the payload hasn't yet expired, the absolute
expiration is returned as an optional out parameter along with the original unprotected data. If the payload is
expired, all overloads of the Unprotect method will throw CryptographicException.
WARNING
It's not advised to use these APIs to protect payloads which require long-term or indefinite persistence. "Can I afford for the
protected payloads to be permanently unrecoverable after a month?" can serve as a good rule of thumb; if the answer is no
then developers should consider alternative APIs.
The sample below uses the non-DI code paths for instantiating the data protection system. To run this sample,
ensure that you have first added a reference to the Microsoft.AspNetCore.DataProtection.Extensions package.
using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;
// wait 6 seconds and perform another unprotect, demonstrating that the payload self-expires
Console.WriteLine("Waiting 6 seconds...");
Thread.Sleep(6000);
timeLimitedProtector.Unprotect(protectedData);
}
}
/*
* SAMPLE OUTPUT
*
* Enter input: Hello!
* Protected data: CfDJ8Hu5z0zwxn...nLk7Ok
* Round-tripped data: Hello!
* Waiting 6 seconds...
* <<throws CryptographicException with message 'The payload expired at ...'>>
*/
Unprotect payloads whose keys have been revoked
in ASP.NET Core
6/21/2018 • 3 minutes to read • Edit Online
The ASP.NET Core data protection APIs are not primarily intended for indefinite persistence of confidential
payloads. Other technologies like Windows CNG DPAPI and Azure Rights Management are more suited to the
scenario of indefinite storage, and they have correspondingly strong key management capabilities. That said,
there's nothing prohibiting a developer from using the ASP.NET Core data protection APIs for long-term
protection of confidential data. Keys are never removed from the key ring, so IDataProtector.Unprotect can
always recover existing payloads as long as the keys are available and valid.
However, an issue arises when the developer tries to unprotect data that has been protected with a revoked key, as
IDataProtector.Unprotect will throw an exception in this case. This might be fine for short-lived or transient
payloads (like authentication tokens), as these kinds of payloads can easily be recreated by the system, and at
worst the site visitor might be required to log in again. But for persisted payloads, having Unprotect throw could
lead to unacceptable data loss.
IPersistedDataProtector
To support the scenario of allowing payloads to be unprotected even in the face of revoked keys, the data
protection system contains an IPersistedDataProtector type. To get an instance of IPersistedDataProtector ,
simply get an instance of IDataProtector in the normal fashion and try casting the IDataProtector to
IPersistedDataProtector .
NOTE
Not all IDataProtector instances can be cast to IPersistedDataProtector . Developers should use the C# as operator or
similar to avoid runtime exceptions caused by invalid casts, and they should be prepared to handle the failure case
appropriately.
This API takes the protected payload (as a byte array) and returns the unprotected payload. There's no string-
based overload. The two out parameters are as follows.
requiresMigration : will be set to true if the key used to protect this payload is no longer the active default
key, e.g., the key used to protect this payload is old and a key rolling operation has since taken place. The
caller may wish to consider reprotecting the payload depending on their business needs.
wasRevoked : will be set to true if the key used to protect this payload was revoked.
WARNING
Exercise extreme caution when passing ignoreRevocationErrors: true to the DangerousUnprotect method. If after
calling this method the wasRevoked value is true, then the key used to protect this payload was revoked, and the payload's
authenticity should be treated as suspect. In this case, only continue operating on the unprotected payload if you have
some separate assurance that it's authentic, e.g. that it's coming from a secure database rather than being sent by an
untrusted web client.
using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;
// get a reference to the key manager and revoke all keys in the key ring
var keyManager = services.GetService<IKeyManager>();
Console.WriteLine("Revoking all keys in the key ring...");
keyManager.RevokeAllKeys(DateTimeOffset.Now, "Sample revocation.");
/*
* SAMPLE OUTPUT
*
* Input: Hello!
* Protected payload: CfDJ8LHIzUCX1ZVBn2BZ...
* Round-tripped payload: Hello!
* Revoking all keys in the key ring...
* Calling Unprotect...
* CryptographicException: The key {...} has been revoked.
* Calling DangerousUnprotect...
* Unprotected payload: Hello!
* Requires migration = True, was revoked = True
*/
Data Protection configuration in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Visit these topics to learn about Data Protection configuration in ASP.NET Core:
Configure ASP.NET Core Data Protection
An overview on configuring ASP.NET Core Data Protection.
Data Protection key management and lifetime
Information on Data Protection key management and lifetime.
Data Protection machine-wide policy support
Details on setting a default machine-wide policy for all apps that use Data Protection.
Non-DI aware scenarios for Data Protection in ASP.NET Core
How to use the DataProtectionProvider concrete type to use Data Protection without going through DI-
specific code paths.
Configure ASP.NET Core Data Protection
9/20/2018 • 9 minutes to read • Edit Online
When the Data Protection system is initialized, it applies default settings based on the operational
environment. These settings are generally appropriate for apps running on a single machine. There are cases
where a developer may want to change the default settings:
The app is spread across multiple machines.
For compliance reasons.
For these scenarios, the Data Protection system offers a rich configuration API.
WARNING
Similar to configuration files, the data protection key ring should be protected using appropriate permissions. You can
choose to encrypt keys at rest, but this doesn't prevent attackers from creating new keys. Consequently, your app's
security is impacted. The storage location configured with Data Protection should have its access limited to the app itself,
similar to the way you would protect configuration files. For example, if you choose to store your key ring on disk, use
file system permissions. Ensure only the identity under which your web app runs has read, write, and create access to
that directory. If you use Azure Table Storage, only the web app should have the ability to read, write, or create new
entries in the table store, etc.
The extension method AddDataProtection returns an IDataProtectionBuilder. IDataProtectionBuilder exposes
extension methods that you can chain together to configure Data Protection options.
ProtectKeysWithAzureKeyVault
To store keys in Azure Key Vault, configure the system with ProtectKeysWithAzureKeyVault in the Startup
class:
Set the key ring storage location (for example, PersistKeysToAzureBlobStorage). The location must be set
because calling ProtectKeysWithAzureKeyVault implements an IXmlEncryptor that disables automatic data
protection settings, including the key ring storage location. The preceding example uses Azure Blob Storage to
persist the key ring. For more information, see Key storage providers: Azure and Redis. You can also persist the
key ring locally with PersistKeysToFileSystem.
The keyIdentifieris the key vault key identifier used for key encryption (for example,
https://contosokeyvault.vault.azure.net/keys/dataprotection/ ).
ProtectKeysWithAzureKeyVault overloads:
ProtectKeysWithAzureKeyVault(IDataProtectionBuilder, KeyVaultClient, String) permits the use of a
KeyVaultClient to enable the data protection system to use the key vault.
ProtectKeysWithAzureKeyVault(IDataProtectionBuilder, String, String, X509Certificate2) permits the use of
a ClientId and X509Certificate to enable the data protection system to use the key vault.
ProtectKeysWithAzureKeyVault(IDataProtectionBuilder, String, String, String) permits the use of a
ClientId and ClientSecret to enable the data protection system to use the key vault.
PersistKeysToFileSystem
To store keys on a UNC share instead of at the %LOCALAPPDATA% default location, configure the system
with PersistKeysToFileSystem:
WARNING
If you change the key persistence location, the system no longer automatically encrypts keys at rest, since it doesn't
know whether DPAPI is an appropriate encryption mechanism.
ProtectKeysWith*
You can configure the system to protect keys at rest by calling any of the ProtectKeysWith* configuration APIs.
Consider the example below, which stores keys on a UNC share and encrypts those keys at rest with a specific
X.509 certificate:
In ASP.NET Core 2.1 or later, you can provide an X509Certificate2 to ProtectKeysWithCertificate, such as a
certificate loaded from a file:
See Key Encryption At Rest for more examples and discussion on the built-in key encryption mechanisms.
UnprotectKeysWithAnyCertificate
In ASP.NET Core 2.1 or later, you can rotate certificates and decrypt keys at rest using an array of
X509Certificate2 certificates with UnprotectKeysWithAnyCertificate:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
.ProtectKeysWithCertificate(
new X509Certificate2("certificate.pfx", "password"));
.UnprotectKeysWithAnyCertificate(
new X509Certificate2("certificate_old_1.pfx", "password_1"),
new X509Certificate2("certificate_old_2.pfx", "password_2"));
}
SetDefaultKeyLifetime
To configure the system to use a key lifetime of 14 days instead of the default 90 days, use
SetDefaultKeyLifetime:
SetApplicationName
By default, the Data Protection system isolates apps from one another, even if they're sharing the same
physical key repository. This prevents the apps from understanding each other's protected payloads. To share
protected payloads between two apps, use SetApplicationName with the same value for each app:
DisableAutomaticKeyGeneration
You may have a scenario where you don't want an app to automatically roll keys (create new keys) as they
approach expiration. One example of this might be apps set up in a primary/secondary relationship, where
only the primary app is responsible for key management concerns and secondary apps simply have a read-
only view of the key ring. The secondary apps can be configured to treat the key ring as read-only by
configuring the system with DisableAutomaticKeyGeneration:
Per-application isolation
When the Data Protection system is provided by an ASP.NET Core host, it automatically isolates apps from
one another, even if those apps are running under the same worker process account and are using the same
master keying material. This is somewhat similar to the IsolateApps modifier from System.Web's
<machineKey> element.
The isolation mechanism works by considering each app on the local machine as a unique tenant, thus the
IDataProtector rooted for any given app automatically includes the app ID as a discriminator. The app's unique
ID comes from one of two places:
1. If the app is hosted in IIS, the unique identifier is the app's configuration path. If an app is deployed in a
web farm environment, this value should be stable assuming that the IIS environments are configured
similarly across all machines in the web farm.
2. If the app isn't hosted in IIS, the unique identifier is the physical path of the app.
The unique identifier is designed to survive resets — both of the individual app and of the machine itself.
This isolation mechanism assumes that the apps are not malicious. A malicious app can always impact any
other app running under the same worker process account. In a shared hosting environment where apps are
mutually untrusted, the hosting provider should take steps to ensure OS -level isolation between apps,
including separating the apps' underlying key repositories.
If the Data Protection system isn't provided by an ASP.NET Core host (for example, if you instantiate it via the
DataProtectionProvider concrete type) app isolation is disabled by default. When app isolation is disabled, all
apps backed by the same keying material can share payloads as long as they provide the appropriate
purposes. To provide app isolation in this environment, call the SetApplicationName method on the
configuration object and provide a unique name for each app.
services.AddDataProtection()
.UseCryptographicAlgorithms(
new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
});
The default EncryptionAlgorithm is AES -256-CBC, and the default ValidationAlgorithm is HMACSHA256. The
default policy can be set by a system administrator via a machine-wide policy, but an explicit call to
UseCryptographicAlgorithms overrides the default policy.
Calling UseCryptographicAlgorithms allows you to specify the desired algorithm from a predefined built-in list.
You don't need to worry about the implementation of the algorithm. In the scenario above, the Data Protection
system attempts to use the CNG implementation of AES if running on Windows. Otherwise, it falls back to the
managed System.Security.Cryptography.Aes class.
You can manually specify an implementation via a call to UseCustomCryptographicAlgorithms.
TIP
Changing algorithms doesn't affect existing keys in the key ring. It only affects newly-generated keys.
serviceCollection.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new ManagedAuthenticatedEncryptorConfiguration()
{
// A type that subclasses SymmetricAlgorithm
EncryptionAlgorithmType = typeof(Aes),
// Specified in bits
EncryptionAlgorithmKeySize = 256,
Generally the *Type properties must point to concrete, instantiable (via a public parameterless ctor)
implementations of SymmetricAlgorithm and KeyedHashAlgorithm, though the system special-cases some
values like typeof(Aes) for convenience.
NOTE
The SymmetricAlgorithm must have a key length of ≥ 128 bits and a block size of ≥ 64 bits, and it must support CBC-
mode encryption with PKCS #7 padding. The KeyedHashAlgorithm must have a digest size of >= 128 bits, and it must
support keys of length equal to the hash algorithm's digest length. The KeyedHashAlgorithm isn't strictly required to be
HMAC.
services.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new CngCbcAuthenticatedEncryptorConfiguration()
{
// Passed to BCryptOpenAlgorithmProvider
EncryptionAlgorithm = "AES",
EncryptionAlgorithmProvider = null,
// Specified in bits
EncryptionAlgorithmKeySize = 256,
// Passed to BCryptOpenAlgorithmProvider
HashAlgorithm = "SHA256",
HashAlgorithmProvider = null
});
NOTE
The symmetric block cipher algorithm must have a key length of >= 128 bits, a block size of >= 64 bits, and it must
support CBC-mode encryption with PKCS #7 padding. The hash algorithm must have a digest size of >= 128 bits and
must support being opened with the BCRYPT_ALG_HANDLE_HMAC_FLAG flag. The *Provider properties can be set to
null to use the default provider for the specified algorithm. See the BCryptOpenAlgorithmProvider documentation for
more information.
services.AddDataProtection()
.UseCustomCryptographicAlgorithms(
new CngGcmAuthenticatedEncryptorConfiguration()
{
// Passed to BCryptOpenAlgorithmProvider
EncryptionAlgorithm = "AES",
EncryptionAlgorithmProvider = null,
// Specified in bits
EncryptionAlgorithmKeySize = 256
});
NOTE
The symmetric block cipher algorithm must have a key length of >= 128 bits, a block size of exactly 128 bits, and it
must support GCM encryption. You can set the EncryptionAlgorithmProvider property to null to use the default
provider for the specified algorithm. See the BCryptOpenAlgorithmProvider documentation for more information.
See also
Non-DI aware scenarios for Data Protection in ASP.NET Core
Data Protection machine-wide policy support in ASP.NET Core
Host ASP.NET Core in a web farm
Data Protection key management and lifetime in
ASP.NET Core
7/16/2018 • 2 minutes to read • Edit Online
By Rick Anderson
Key management
The app attempts to detect its operational environment and handle key configuration on its own.
1. If the app is hosted in Azure Apps, keys are persisted to the %HOME%\ASP.NET\DataProtection-Keys
folder. This folder is backed by network storage and is synchronized across all machines hosting the app.
Keys aren't protected at rest.
The DataProtection-Keys folder supplies the key ring to all instances of an app in a single deployment
slot.
Separate deployment slots, such as Staging and Production, don't share a key ring. When you swap
between deployment slots, for example swapping Staging to Production or using A/B testing, any app
using Data Protection won't be able to decrypt stored data using the key ring inside the previous slot.
This leads to users being logged out of an app that uses the standard ASP.NET Core cookie
authentication, as it uses Data Protection to protect its cookies. If you desire slot-independent key rings,
use an external key ring provider, such as Azure Blob Storage, Azure Key Vault, a SQL store, or Redis
cache.
2. If the user profile is available, keys are persisted to the %LOCALAPPDATA%\ASP.NET\DataProtection-
Keys folder. If the operating system is Windows, the keys are encrypted at rest using DPAPI.
3. If the app is hosted in IIS, keys are persisted to the HKLM registry in a special registry key that's ACLed
only to the worker process account. Keys are encrypted at rest using DPAPI.
4. If none of these conditions match, keys aren't persisted outside of the current process. When the process
shuts down, all generated keys are lost.
The developer is always in full control and can override how and where keys are stored. The first three options
above should provide good defaults for most apps similar to how the ASP.NET <machineKey> auto-generation
routines worked in the past. The final, fallback option is the only scenario that requires the developer to specify
configuration upfront if they want key persistence, but this fallback only occurs in rare situations.
When hosting in a Docker container, keys should be persisted in a folder that's a Docker volume (a shared
volume or a host-mounted volume that persists beyond the container's lifetime) or in an external provider, such
as Azure Key Vault or Redis. An external provider is also useful in web farm scenarios if apps can't access a shared
network volume (see PersistKeysToFileSystem for more information).
WARNING
If the developer overrides the rules outlined above and points the Data Protection system at a specific key repository,
automatic encryption of keys at rest is disabled. At-rest protection can be re-enabled via configuration.
Key lifetime
Keys have a 90-day lifetime by default. When a key expires, the app automatically generates a new key and sets
the new key as the active key. As long as retired keys remain on the system, your app can decrypt any data
protected with them. See key management for more information.
Default algorithms
The default payload protection algorithm used is AES -256-CBC for confidentiality and HMACSHA256 for
authenticity. A 512-bit master key, changed every 90 days, is used to derive the two sub-keys used for these
algorithms on a per-payload basis. See subkey derivation for more information.
Additional resources
Key management extensibility in ASP.NET Core
Host ASP.NET Core in a web farm
Data Protection machine-wide policy support in
ASP.NET Core
6/21/2018 • 3 minutes to read • Edit Online
By Rick Anderson
When running on Windows, the Data Protection system has limited support for setting a default machine-wide
policy for all apps that consume ASP.NET Core Data Protection. The general idea is that an administrator might
wish to change a default setting, such as the algorithms used or key lifetime, without the need to manually update
every app on the machine.
WARNING
The system administrator can set default policy, but they can't enforce it. The app developer can always override any value
with one of their own choosing. The default policy only affects apps where the developer hasn't specified an explicit value
for a setting.
Encryption types
If EncryptionType is CNG -CBC, the system is configured to use a CBC -mode symmetric block cipher for
confidentiality and HMAC for authenticity with services provided by Windows CNG (see Specifying custom
Windows CNG algorithms for more details). The following additional values are supported, each of which
corresponds to a property on the CngCbcAuthenticatedEncryptionSettings type.
If EncryptionType is CNG -GCM, the system is configured to use a Galois/Counter Mode symmetric block cipher
for confidentiality and authenticity with services provided by Windows CNG (see Specifying custom Windows
CNG algorithms for more details). The following additional values are supported, each of which corresponds to a
property on the CngGcmAuthenticatedEncryptionSettings type.
If EncryptionType has any other value other than null or empty, the Data Protection system throws an exception
at startup.
WARNING
When configuring a default policy setting that involves type names (EncryptionAlgorithmType, ValidationAlgorithmType,
KeyEscrowSinks), the types must be available to the app. This means that for apps running on Desktop CLR, the assemblies
that contain these types should be present in the Global Assembly Cache (GAC). For ASP.NET Core apps running on .NET
Core, the packages that contain these types should be installed.
Non-DI aware scenarios for Data Protection in
ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
By Rick Anderson
The ASP.NET Core Data Protection system is normally added to a service container and consumed by dependent
components via dependency injection (DI). However, there are cases where this isn't feasible or desired, especially
when importing the system into an existing app.
To support these scenarios, the Microsoft.AspNetCore.DataProtection.Extensions package provides a concrete
type, DataProtectionProvider, which offers a simple way to use Data Protection without relying on DI. The
DataProtectionProvider type implements IDataProtectionProvider. Constructing DataProtectionProvider only
requires providing a DirectoryInfo instance to indicate where the provider's cryptographic keys should be stored,
as seen in the following code sample:
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;
Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
/*
* SAMPLE OUTPUT
*
* Enter input: Hello world!
* Protect returned: CfDJ8FWbAn6...ch3hAPm1NJA
* Unprotect returned: Hello world!
*
* Press any key...
*/
By default, the DataProtectionProvider concrete type doesn't encrypt raw key material before persisting it to the
file system. This is to support scenarios where the developer points to a network share and the Data Protection
system can't automatically deduce an appropriate at-rest key encryption mechanism.
Additionally, the DataProtectionProvider concrete type doesn't isolate apps by default. All apps using the same
key directory can share payloads as long as their purpose parameters match.
The DataProtectionProvider constructor accepts an optional configuration callback that can be used to adjust the
behaviors of the system. The sample below demonstrates restoring isolation with an explicit call to
SetApplicationName. The sample also demonstrates configuring the system to automatically encrypt persisted
keys using Windows DPAPI. If the directory points to a UNC share, you may wish to distribute a shared
certificate across all relevant machines and to configure the system to use certificate-based encryption with a call
to ProtectKeysWithCertificate.
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;
Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
TIP
Instances of the DataProtectionProvider concrete type are expensive to create. If an app maintains multiple instances of
this type and if they're all using the same key storage directory, app performance might degrade. If you use the
DataProtectionProvider type, we recommend that you create this type once and reuse it as much as possible. The
DataProtectionProvider type and all IDataProtector instances created from it are thread-safe for multiple callers.
ASP.NET Core Data Protection extensibility APIs
6/21/2018 • 2 minutes to read • Edit Online
WARNING
Types that implement any of the following interfaces should be thread-safe for multiple callers.
IAuthenticatedEncryptor
The IAuthenticatedEncryptor interface is the basic building block of the cryptographic subsystem. There's
generally one IAuthenticatedEncryptor per key, and the IAuthenticatedEncryptor instance wraps all cryptographic
key material and algorithmic information necessary to perform cryptographic operations.
As its name suggests, the type is responsible for providing authenticated encryption and decryption services. It
exposes the following two APIs.
Decrypt(ArraySegment ciphertext, ArraySegment additionalAuthenticatedData) : byte[]
Encrypt(ArraySegment plaintext, ArraySegment additionalAuthenticatedData) : byte[]
The Encrypt method returns a blob that includes the enciphered plaintext and an authentication tag. The
authentication tag must encompass the additional authenticated data (AAD ), though the AAD itself need not be
recoverable from the final payload. The Decrypt method validates the authentication tag and returns the
deciphered payload. All failures (except ArgumentNullException and similar) should be homogenized to
CryptographicException.
NOTE
The IAuthenticatedEncryptor instance itself doesn't actually need to contain the key material. For example, the
implementation could delegate to an HSM for all operations.
XML Serialization
The primary difference between IAuthenticatedEncryptor and IAuthenticatedEncryptorDescriptor is that the
descriptor knows how to create the encryptor and supply it with valid arguments. Consider an
IAuthenticatedEncryptor whose implementation relies on SymmetricAlgorithm and KeyedHashAlgorithm. The
encryptor's job is to consume these types, but it doesn't necessarily know where these types came from, so it can't
really write out a proper description of how to recreate itself if the application restarts. The descriptor acts as a
higher level on top of this. Since the descriptor knows how to create the encryptor instance (e.g., it knows how to
create the required algorithms), it can serialize that knowledge in XML form so that the encryptor instance can be
recreated after an application reset.
The descriptor can be serialized via its ExportToXml routine. This routine returns an XmlSerializedDescriptorInfo
which contains two properties: the XElement representation of the descriptor and the Type which represents an
IAuthenticatedEncryptorDescriptorDeserializer which can be used to resurrect this descriptor given the
corresponding XElement.
The serialized descriptor may contain sensitive information such as cryptographic key material. The data
protection system has built-in support for encrypting information before it's persisted to storage. To take
advantage of this, the descriptor should mark the element which contains sensitive information with the attribute
name "requiresEncryption" (xmlns "http://schemas.asp.net/2015/03/dataProtection"), value "true".
TIP
There's a helper API for setting this attribute. Call the extension method XElement.MarkAsRequiresEncryption() located in
namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.
There can also be cases where the serialized descriptor doesn't contain sensitive information. Consider again the
case of a cryptographic key stored in an HSM. The descriptor cannot write out the key material when serializing
itself since the HSM won't expose the material in plaintext form. Instead, the descriptor might write out the key-
wrapped version of the key (if the HSM allows export in this fashion) or the HSM's own unique identifier for the
key.
IAuthenticatedEncryptorDescriptorDeserializer
The IAuthenticatedEncryptorDescriptorDeserializer interface represents a type that knows how to deserialize
an IAuthenticatedEncryptorDescriptor instance from an XElement. It exposes a single method:
ImportFromXml(XElement element) : IAuthenticatedEncryptorDescriptor
The ImportFromXml method takes the XElement that was returned by
IAuthenticatedEncryptorDescriptor.ExportToXml and creates an equivalent of the original
IAuthenticatedEncryptorDescriptor.
Types which implement IAuthenticatedEncryptorDescriptorDeserializer should have one of the following two
public constructors:
.ctor(IServiceProvider)
.ctor()
NOTE
The IServiceProvider passed to the constructor may be null.
TIP
Read the key management section before reading this section, as it explains some of the fundamental concepts behind these
APIs.
WARNING
Types that implement any of the following interfaces should be thread-safe for multiple callers.
Key
The IKey interface is the basic representation of a key in cryptosystem. The term key is used here in the abstract
sense, not in the literal sense of "cryptographic key material". A key has the following properties:
Activation, creation, and expiration dates
Revocation status
Key identifier (a GUID )
Additionally, IKey exposes a CreateEncryptor method which can be used to create an IAuthenticatedEncryptor
instance tied to this key.
Additionally, IKey exposes a CreateEncryptorInstance method which can be used to create an
IAuthenticatedEncryptor instance tied to this key.
NOTE
There's no API to retrieve the raw cryptographic material from an IKey instance.
IKeyManager
The IKeyManager interface represents an object responsible for general key storage, retrieval, and manipulation. It
exposes three high-level operations:
Create a new key and persist it to storage.
Get all keys from storage.
Revoke one or more keys and persist the revocation information to storage.
WARNING
Writing an IKeyManager is a very advanced task, and the majority of developers shouldn't attempt it. Instead, most
developers should take advantage of the facilities offered by the XmlKeyManager class.
XmlKeyManager
The XmlKeyManager type is the in-box concrete implementation of IKeyManager . It provides several useful facilities,
including key escrow and encryption of keys at rest. Keys in this system are represented as XML elements
(specifically, XElement).
XmlKeyManager depends on several other components in the course of fulfilling its tasks:
AlgorithmConfiguration , which dictates the algorithms used by new keys.
IXmlRepository , which controls where keys are persisted in storage.
IXmlEncryptor [optional], which allows encrypting keys at rest.
IKeyEscrowSink [optional], which provides key escrow services.
Below are high-level diagrams which indicate how these components are wired together within XmlKeyManager .
IXmlRepository
The IXmlRepository interface represents a type that can persist XML to and retrieve XML from a backing store. It
exposes two APIs:
GetAllElements : IReadOnlyCollection<XElement>
StoreElement(XElement element, string friendlyName)
Implementations of IXmlRepository don't need to parse the XML passing through them. They should treat the
XML documents as opaque and let higher layers worry about generating and parsing the documents.
There are four built-in concrete types which implement IXmlRepository :
FileSystemXmlRepository
RegistryXmlRepository
AzureStorage.AzureBlobXmlRepository
RedisXmlRepository
See the key storage providers document for more information.
Registering a custom IXmlRepository is appropriate when using a different backing store (for example, Azure
Table Storage).
To change the default repository application-wide, register a custom IXmlRepository instance:
services.AddSingleton<IXmlRepository>(new MyCustomXmlRepository());
IXmlEncryptor
The IXmlEncryptor interface represents a type that can encrypt a plaintext XML element. It exposes a single API:
Encrypt(XElement plaintextElement) : EncryptedXmlInfo
If a serialized contains any elements marked as "requires encryption", then
IAuthenticatedEncryptorDescriptor
XmlKeyManager will run those elements through the configured IXmlEncryptor 's Encrypt method, and it will
persist the enciphered element rather than the plaintext element to the IXmlRepository . The output of the
Encrypt method is an EncryptedXmlInfo object. This object is a wrapper which contains both the resultant
enciphered XElement and the Type which represents an IXmlDecryptor which can be used to decipher the
corresponding element.
There are four built-in concrete types which implement IXmlEncryptor :
CertificateXmlEncryptor
DpapiNGXmlEncryptor
DpapiXmlEncryptor
NullXmlEncryptor
See the key encryption at rest document for more information.
To change the default key-encryption-at-rest mechanism application-wide, register a custom IXmlEncryptor
instance:
services.AddSingleton<IXmlEncryptor>(new MyCustomXmlEncryptor());
IXmlDecryptor
The IXmlDecryptor interface represents a type that knows how to decrypt an XElement that was enciphered via an
IXmlEncryptor . It exposes a single API:
Types which implement IXmlDecryptor should have one of the following two public constructors:
.ctor(IServiceProvider)
.ctor()
NOTE
The IServiceProvider passed to the constructor may be null.
IKeyEscrowSink
The IKeyEscrowSink interface represents a type that can perform escrow of sensitive information. Recall that
serialized descriptors might contain sensitive information (such as cryptographic material), and this is what led to
the introduction of the IXmlEncryptor type in the first place. However, accidents happen, and key rings can be
deleted or become corrupted.
The escrow interface provides an emergency escape hatch, allowing access to the raw serialized XML before it's
transformed by any configured IXmlEncryptor. The interface exposes a single API:
Store(Guid keyId, XElement element)
It's up to the IKeyEscrowSink implementation to handle the provided element in a secure manner consistent with
business policy. One possible implementation could be for the escrow sink to encrypt the XML element using a
known corporate X.509 certificate where the certificate's private key has been escrowed; the
CertificateXmlEncryptor type can assist with this. The IKeyEscrowSink implementation is also responsible for
persisting the provided element appropriately.
By default no escrow mechanism is enabled, though server administrators can configure this globally. It can also
be configured programmatically via the IDataProtectionBuilder.AddKeyEscrowSink method as shown in the sample
below. The AddKeyEscrowSink method overloads mirror the IServiceCollection.AddSingleton and
IServiceCollection.AddInstance overloads, as IKeyEscrowSink instances are intended to be singletons. If multiple
IKeyEscrowSink instances are registered, each one will be called during key generation, so keys can be escrowed
to multiple mechanisms simultaneously.
There's no API to read material from an IKeyEscrowSink instance. This is consistent with the design theory of the
escrow mechanism: it's intended to make the key material accessible to a trusted authority, and since the
application is itself not a trusted authority, it shouldn't have access to its own escrowed material.
The following sample code demonstrates creating and registering an IKeyEscrowSink where keys are escrowed
such that only members of "CONTOSODomain Admins" can recover them.
NOTE
To run this sample, you must be on a domain-joined Windows 8 / Windows Server 2012 machine, and the domain controller
must be Windows Server 2012 or later.
using System;
using System.IO;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.DependencyInjection;
// get a reference to the key manager and force a new key to be generated
Console.WriteLine("Generating new key...");
var keyManager = services.GetService<IKeyManager>();
keyManager.CreateNewKey(
activationDate: DateTimeOffset.Now,
expirationDate: DateTimeOffset.Now.AddDays(7));
}
// A key escrow sink where keys are escrowed such that they
// can be read by members of the CONTOSO\Domain Admins group.
private class MyKeyEscrowSink : IKeyEscrowSink
{
private readonly IXmlEncryptor _escrowEncryptor;
/*
* SAMPLE OUTPUT
*
* Generating new key...
* Escrowing key 38e74534-c1b8-4b43-aea1-79e856a822e5
* <encryptedKey>
* <!-- This key is encrypted with Windows DPAPI-NG. -->
* <!-- Rule: SID=S-1-5-21-1004336348-1177238915-682003330-512 -->
* <value>MIIIfAYJKoZIhvcNAQcDoIIIbTCCCGkCAQ...T5rA4g==</value>
* </encryptedKey>
*/
Miscellaneous ASP.NET Core Data Protection APIs
6/21/2018 • 2 minutes to read • Edit Online
WARNING
Types that implement any of the following interfaces should be thread-safe for multiple callers.
ISecret
The ISecret interface represents a secret value, such as cryptographic key material. It contains the following API
surface:
Length : int
Dispose() : void
The WriteSecretIntoBuffer method populates the supplied buffer with the raw secret value. The reason this API
takes the buffer as a parameter rather than returning a byte[] directly is that this gives the caller the opportunity
to pin the buffer object, limiting secret exposure to the managed garbage collector.
The Secret type is a concrete implementation of ISecret where the secret value is stored in in-process memory.
On Windows platforms, the secret value is encrypted via CryptProtectMemory.
ASP.NET Core Data Protection implementation
6/21/2018 • 2 minutes to read • Edit Online
Calls to IDataProtector.Protect are authenticated encryption operations. The Protect method offers both
confidentiality and authenticity, and it's tied to the purpose chain that was used to derive this particular
IDataProtector instance from its root IDataProtectionProvider.
IDataProtector.Protect takes a byte[] plaintext parameter and produces a byte[] protected payload, whose format
is described below. (There's also an extension method overload which takes a string plaintext parameter and
returns a string protected payload. If this API is used the protected payload format will still have the below
structure, but it will be base64url-encoded.)
09 F0 C9 F0 80 9C 81 0C 19 66 19 40 95 36 53 F8
AA FF EE 57 57 2F 40 4C 3F 7F CC 9D CC D9 32 3E
84 17 99 16 EC BA 1F 4A A1 18 45 1F 2D 13 7A 28
79 6B 86 9C F8 B7 84 F9 26 31 FC B1 86 0A F1 56
61 CF 14 58 D3 51 6F CF 36 50 85 82 08 2D 3F 73
5F B0 AD 9E 1A B2 AE 13 57 90 C8 F5 7C 95 4E 6A
8A AA 06 EF 43 CA 19 62 84 7C 11 B2 C8 71 9D AA
52 19 2E 5B 4C 1E 54 F0 55 BE 88 92 12 C1 4B 5E
52 C9 74 A0
From the payload format above the first 32 bits, or 4 bytes are the magic header identifying the version (09 F0 C9
F0)
The next 128 bits, or 16 bytes is the key identifier (80 9C 81 0C 19 66 19 40 95 36 53 F8 AA FF EE 57)
The remainder contains the payload and is specific to the format used.
WARNING
All payloads protected to a given key will begin with the same 20-byte (magic value, key id) header. Administrators can use
this fact for diagnostic purposes to approximate when a payload was generated. For example, the payload above
corresponds to key {0c819c80-6619-4019-9536-53f8aaffee57}. If after checking the key repository you find that this
specific key's activation date was 2015-01-01 and its expiration date was 2015-03-01, then it's reasonable to assume that
the payload (if not tampered with) was generated within that window, give or take a small fudge factor on either side.
Subkey derivation and authenticated encryption in
ASP.NET Core
6/21/2018 • 3 minutes to read • Edit Online
Most keys in the key ring will contain some form of entropy and will have algorithmic information stating "CBC -
mode encryption + HMAC validation" or "GCM encryption + validation". In these cases, we refer to the embedded
entropy as the master keying material (or KM ) for this key, and we perform a key derivation function to derive the
keys that will be used for the actual cryptographic operations.
NOTE
Keys are abstract, and a custom implementation might not behave as below. If the key provides its own implementation of
IAuthenticatedEncryptor rather than using one of our built-in factories, the mechanism described in this section no
longer applies.
NOTE
The IDataProtector.Protect implementation will prepend the magic header and key id to output before returning it to
the caller. Because the magic header and key id are implicitly part of AAD, and because the key modifier is fed as input to the
KDF, this means that every single byte of the final returned payload is authenticated by the MAC.
5B B6 C9 83 13 78 22 1D 8E 10 73 CA CF 65 8E B0
61 62 42 71 CB 83 21 DD A0 4A 05 00 5B AB C0 A2
49 6F A5 61 E3 E2 49 87 AA 63 55 CD 74 0A DA C4
B7 92 3D BF 59 90 00 A9
Next, compute Enc_CBC (K_E, IV, "") for AES -192-CBC given IV = 0* and K_E as above.
result := F474B1872B3B53E4721DE19C0841DB6F
Next, compute MAC (K_H, "") for HMACSHA256 given K_H as above.
result := D4791184B996092EE1202F36E8608FA8FBD98ABDFF5402F264B1D7211536220C
This produces the full context header below:
00 00 00 00 00 18 00 00 00 10 00 00 00 20 00 00
00 20 F4 74 B1 87 2B 3B 53 E4 72 1D E1 9C 08 41
DB 6F D4 79 11 84 B9 96 09 2E E1 20 2F 36 E8 60
8F A8 FB D9 8A BD FF 54 02 F2 64 B1 D7 21 15 36
22 0C
This context header is the thumbprint of the authenticated encryption algorithm pair (AES -192-CBC encryption +
HMACSHA256 validation). The components, as described above are:
the marker (00 00)
the block cipher key length (00 00 00 18)
the block cipher block size (00 00 00 10)
the HMAC key length (00 00 00 20)
the HMAC digest size (00 00 00 20)
the block cipher PRP output (F4 74 - DB 6F ) and
the HMAC PRF output (D4 79 - end).
NOTE
The CBC-mode encryption + HMAC authentication context header is built the same way regardless of whether the
algorithms implementations are provided by Windows CNG or by managed SymmetricAlgorithm and KeyedHashAlgorithm
types. This allows applications running on different operating systems to reliably produce the same context header even
though the implementations of the algorithms differ between OSes. (In practice, the KeyedHashAlgorithm doesn't have to be
a proper HMAC. It can be any keyed hash algorithm type.)
A2 19 60 2F 83 A9 13 EA B0 61 3A 39 B8 A6 7E 22
61 D9 F8 6C 10 51 E2 BB DC 4A 00 D7 03 A2 48 3E
D1 F7 5A 34 EB 28 3E D7 D4 67 B4 64
Next, compute Enc_CBC (K_E, IV, "") for 3DES -192-CBC given IV = 0* and K_E as above.
result := ABB100F81E53E10E
Next, compute MAC (K_H, "") for HMACSHA1 given K_H as above.
result := 76EB189B35CF03461DDF877CD9F4B1B4D63A7555
This produces the full context header which is a thumbprint of the authenticated encryption algorithm pair (3DES -
192-CBC encryption + HMACSHA1 validation), shown below:
00 00 00 00 00 18 00 00 00 08 00 00 00 14 00 00
00 14 AB B1 00 F8 1E 53 E1 0E 76 EB 18 9B 35 CF
03 46 1D DF 87 7C D9 F4 B1 B4 D6 3A 75 55
00 01 00 00 00 20 00 00 00 0C 00 00 00 10 00 00
00 10 E7 DC CE 66 DF 85 5A 32 3A 6B B7 BD 7A 59
BE 45
The data protection system automatically manages the lifetime of master keys used to protect and unprotect
payloads. Each key can exist in one of four stages:
Created - the key exists in the key ring but has not yet been activated. The key shouldn't be used for new
Protect operations until sufficient time has elapsed that the key has had a chance to propagate to all
machines that are consuming this key ring.
Active - the key exists in the key ring and should be used for all new Protect operations.
Expired - the key has run its natural lifetime and should no longer be used for new Protect operations.
Revoked - the key is compromised and must not be used for new Protect operations.
Created, active, and expired keys may all be used to unprotect incoming payloads. Revoked keys by default may
not be used to unprotect payloads, but the application developer can override this behavior if necessary.
WARNING
The developer might be tempted to delete a key from the key ring (e.g., by deleting the corresponding file from the file
system). At that point, all data protected by the key is permanently undecipherable, and there's no emergency override like
there's with revoked keys. Deleting a key is truly destructive behavior, and consequently the data protection system
exposes no first-class API for performing this operation.
services.AddDataProtection()
// use 14-day lifetime instead of 90-day lifetime
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
An administrator can also change the default system-wide, though an explicit call to SetDefaultKeyLifetime will
override any system-wide policy. The default key lifetime cannot be shorter than 7 days.
WARNING
Developers should very rarely (if ever) need to use the key management APIs directly. The data protection system will
perform automatic key management as described above.
The data protection system exposes an interface IKeyManager that can be used to inspect and make changes to
the key ring. The DI system that provided the instance of IDataProtectionProvider can also provide an instance
of IKeyManager for your consumption. Alternatively, you can pull the IKeyManager straight from the
IServiceProvider as in the example below.
Any operation which modifies the key ring (creating a new key explicitly or performing a revocation) will
invalidate the in-memory cache. The next call to Protect or Unprotect will cause the data protection system to
reread the key ring and recreate the cache.
The sample below demonstrates using the IKeyManager interface to inspect and manipulate the key ring,
including revoking existing keys and generating a new key manually.
using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;
// add a new key to the key ring with immediate activation and a 1-month expiration
keyManager.CreateNewKey(
activationDate: DateTimeOffset.Now,
expirationDate: DateTimeOffset.Now.AddMonths(1));
Console.WriteLine("Added a new key.");
/*
* SAMPLE OUTPUT
*
* Performed a protect operation.
* The key ring contains 1 key(s).
* Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = False
* Revoked all existing keys.
* Added a new key.
* The key ring contains 2 key(s).
* Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = True
* Key {2266fc40-e2fb-48c6-8ce2-5fde6b1493f7}: Created = 2015-03-18 22:20:51Z, IsRevoked = False
*/
Key storage
The data protection system has a heuristic whereby it attempts to deduce an appropriate key storage location
and encryption-at-rest mechanism automatically. The key persistence mechanism is also configurable by the app
developer. The following documents discuss the in-box implementations of these mechanisms:
Key storage providers in ASP.NET Core
Key encryption At rest in ASP.NET Core
Key storage providers in ASP.NET Core
9/27/2018 • 2 minutes to read • Edit Online
The data protection system employs a discovery mechanism by default to determine where cryptographic keys
should be persisted. The developer can override the default discovery mechanism and manually specify the
location.
WARNING
If you specify an explicit key persistence location, the data protection system deregisters the default key encryption at rest
mechanism, so keys are no longer encrypted at rest. It's recommended that you additionally specify an explicit key
encryption mechanism for production deployments.
File system
To configure a file system-based key repository, call the PersistKeysToFileSystem configuration routine as shown
below. Provide a DirectoryInfo pointing to the repository where keys should be stored:
The DirectoryInfo can point to a directory on the local machine, or it can point to a folder on a network share. If
pointing to a directory on the local machine (and the scenario is that only apps on the local machine require
access to use this repository), consider using Windows DPAPI (on Windows) to encrypt the keys at rest.
Otherwise, consider using an X.509 certificate to encrypt keys at rest.
Registry
Only applies to Windows deployments.
Sometimes the app might not have write access to the file system. Consider a scenario where an app is running
as a virtual service account (such as w3wp.exe's app pool identity). In these cases, the administrator can
provision a registry key that's accessible by the service account identity. Call the PersistKeysToRegistry extension
method as shown below. Provide a RegistryKey pointing to the location where cryptographic keys should be
stored:
IMPORTANT
We recommend using Windows DPAPI to encrypt the keys at rest.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
// using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.PersistKeysToDbContext<MyKeysContext>();
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
The generic parameter, TContext , must inherit from DbContext and IDataProtectionKeyContext:
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApp1.Data;
namespace WebApp1
{
class MyKeysContext : DbContext, IDataProtectionKeyContext
{
// A recommended constructor overload when using EF Core
// with dependency injection.
public MyKeysContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
The data protection system employs a discovery mechanism by default to determine how cryptographic keys
should be encrypted at rest. The developer can override the discovery mechanism and manually specify how
keys should be encrypted at rest.
WARNING
If you specify an explicit key persistence location, the data protection system deregisters the default key encryption at
rest mechanism. Consequently, keys are no longer encrypted at rest. We recommend that you specify an explicit key
encryption mechanism for production deployments. The encryption-at-rest mechanism options are described in this
topic.
For more information, see Configure ASP.NET Core Data Protection: ProtectKeysWithAzureKeyVault.
Windows DPAPI
Only applies to Windows deployments.
When Windows DPAPI is used, key material is encrypted with CryptProtectData before being persisted to
storage. DPAPI is an appropriate encryption mechanism for data that's never read outside of the current
machine (though it's possible to back these keys up to Active Directory; see DPAPI and Roaming Profiles). To
configure DPAPI key-at-rest encryption, call one of the ProtectKeysWithDpapi extension methods:
If ProtectKeysWithDpapi is called with no parameters, only the current Windows user account can decipher the
persisted key ring. You can optionally specify that any user account on the machine (not just the current user
account) be able to decipher the key ring:
public void ConfigureServices(IServiceCollection services)
{
// All user accounts on the machine can decrypt the keys
services.AddDataProtection()
.ProtectKeysWithDpapi(protectToLocalMachine: true);
}
X.509 certificate
If the app is spread across multiple machines, it may be convenient to distribute a shared X.509 certificate
across the machines and configure the hosted apps to use the certificate for encryption of keys at rest:
Due to .NET Framework limitations, only certificates with CAPI private keys are supported. See the content
below for possible workarounds to these limitations.
Windows DPAPI-NG
This mechanism is available only on Windows 8/Windows Server 2012 or later.
Beginning with Windows 8, Windows OS supports DPAPI-NG (also called CNG DPAPI). For more
information, see About CNG DPAPI.
The principal is encoded as a protection descriptor rule. In the following example that calls
ProtectKeysWithDpapiNG, only the domain-joined user with the specified SID can decrypt the key ring:
There's also a parameterless overload of ProtectKeysWithDpapiNG . Use this convenience method to specify the
rule "SID={CURRENT_ACCOUNT_SID }", where CURRENT_ACCOUNT_SID is the SID of the current
Windows user account:
In this scenario, the AD domain controller is responsible for distributing the encryption keys used by the
DPAPI-NG operations. The target user can decipher the encrypted payload from any domain-joined machine
(provided that the process is running under their identity).
Any app pointed at this repository must be running on Windows 8.1/Windows Server 2012 R2 or later to
decipher the keys.
Once an object is persisted to the backing store, its representation is forever fixed. New data can be added to the
backing store, but existing data can never be mutated. The primary purpose of this behavior is to prevent data
corruption.
One consequence of this behavior is that once a key is written to the backing store, it's immutable. Its creation,
activation, and expiration dates can never be changed, though it can revoked by using IKeyManager . Additionally,
its underlying algorithmic information, master keying material, and encryption at rest properties are also
immutable.
If the developer changes any setting that affects key persistence, those changes won't go into effect until the next
time a key is generated, either via an explicit call to IKeyManager.CreateNewKey or via the data protection system's
own automatic key generation behavior. The settings that affect key persistence are as follows:
The default key lifetime
The key encryption at rest mechanism
The algorithmic information contained within the key
If you need these settings to kick in earlier than the next automatic key rolling time, consider making an explicit
call to IKeyManager.CreateNewKey to force the creation of a new key. Remember to provide an explicit activation
date ({ now + 2 days } is a good rule of thumb to allow time for the change to propagate) and expiration date in
the call.
TIP
All applications touching the repository should specify the same settings with the IDataProtectionBuilder extension
methods. Otherwise, the properties of the persisted key will be dependent on the particular application that invoked the key
generation routines.
Key storage format in ASP.NET Core
7/24/2018 • 2 minutes to read • Edit Online
Objects are stored at rest in XML representation. The default directory for key storage is
%LOCALAPPDATA%\ASP.NET\DataProtection-Keys.
The <key> element contains the following attributes and child elements:
The key id. This value is treated as authoritative; the filename is simply a nicety for human readability.
The version of the <key> element, currently fixed at 1.
The key's creation, activation, and expiration dates.
A <descriptor> element, which contains information on the authenticated encryption implementation
contained within this key.
In the above example, the key's id is {80732141-ec8f-4b80-af9c-c4d2d1ff8901}, it was created and activated on
March 19, 2015, and it has a lifetime of 90 days. (Occasionally the activation date might be slightly before the
creation date as in this example. This is due to a nit in how the APIs work and is harmless in practice.)
In this case, only the specified key is revoked. If the key id is "*", however, as in the below example, all keys whose
creation date is prior to the specified revocation date are revoked.
The <reason> element is never read by the system. It's simply a convenient place to store a human-readable
reason for revocation.
Ephemeral data protection providers in ASP.NET
Core
6/21/2018 • 2 minutes to read • Edit Online
There are scenarios where an application needs a throwaway IDataProtectionProvider . For example, the
developer might just be experimenting in a one-off console application, or the application itself is transient (it's
scripted or a unit test project). To support these scenarios the Microsoft.AspNetCore.DataProtection package
includes a type EphemeralDataProtectionProvider . This type provides a basic implementation of
IDataProtectionProvider whose key repository is held solely in-memory and isn't written out to any backing store.
Each instance of EphemeralDataProtectionProvider uses its own unique master key. Therefore, if an IDataProtector
rooted at an EphemeralDataProtectionProvider generates a protected payload, that payload can only be
unprotected by an equivalent IDataProtector (given the same purpose chain) rooted at the same
EphemeralDataProtectionProvider instance.
The following sample demonstrates instantiating an EphemeralDataProtectionProvider and using it to protect and
unprotect data.
using System;
using Microsoft.AspNetCore.DataProtection;
/*
* SAMPLE OUTPUT
*
* Enter input: Hello!
* Protect returned: CfDJ8AAAAAAAAAAAAAAAAAAAAA...uGoxWLjGKtm1SkNACQ
* Unprotect returned: Hello!
* << throws CryptographicException >>
*/
Compatibility in ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
The implementation of the <machineKey> element in ASP.NET is replaceable. This allows most calls to ASP.NET
cryptographic routines to be routed through a replacement data protection mechanism, including the new data
protection system.
Package installation
NOTE
The new data protection system can only be installed into an existing ASP.NET application targeting .NET 4.5.1 or higher.
Installation will fail if the application targets .NET 4.5 or lower.
To install the new data protection system into an existing ASP.NET 4.5.1+ project, install the package
Microsoft.AspNetCore.DataProtection.SystemWeb. This will instantiate the data protection system using the
default configuration settings.
When you install the package, it inserts a line into Web.config that tells ASP.NET to use it for most cryptographic
operations, including forms authentication, view state, and calls to MachineKey.Protect. The line that's inserted
reads as follows.
TIP
You can tell if the new data protection system is active by inspecting fields like __VIEWSTATE , which should begin with
"CfDJ8" as in the example below. "CfDJ8" is the base64 representation of the magic "09 F0 C9 F0" header that identifies a
payload protected by the data protection system.
Package configuration
The data protection system is instantiated with a default zero-setup configuration. However, since by default keys
are persisted to the local file system, this won't work for applications which are deployed in a farm. To resolve this,
you can provide configuration by creating a type which subclasses DataProtectionStartup and overrides its
ConfigureServices method.
Below is an example of a custom data protection startup type which configured both where keys are persisted and
how they're encrypted at rest. It also overrides the default app isolation policy by providing its own application
name.
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.SystemWeb;
using Microsoft.Extensions.DependencyInjection;
namespace DataProtectionDemo
{
public class MyDataProtectionStartup : DataProtectionStartup
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection()
.SetApplicationName("my-app")
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\myapp-keys\"))
.ProtectKeysWithCertificate("thumbprint");
}
}
}
TIP
You can also use <machineKey applicationName="my-app" ... /> in place of an explicit call to SetApplicationName. This is
a convenience mechanism to avoid forcing the developer to create a DataProtectionStartup-derived type if all they wanted
to configure was setting the application name.
To enable this custom configuration, go back to Web.config and look for the <appSettings> element that the
package install added to the config file. It will look like the following markup:
<appSettings>
<!--
If you want to customize the behavior of the ASP.NET Core Data Protection stack, set the
"aspnet:dataProtectionStartupType" switch below to be the fully-qualified name of a
type which subclasses Microsoft.AspNetCore.DataProtection.SystemWeb.DataProtectionStartup.
-->
<add key="aspnet:dataProtectionStartupType" value="" />
</appSettings>
Fill in the blank value with the assembly-qualified name of the DataProtectionStartup-derived type you just
created. If the name of the application is DataProtectionDemo, this would look like the below.
<add key="aspnet:dataProtectionStartupType"
value="DataProtectionDemo.MyDataProtectionStartup, DataProtectionDemo" />
The newly-configured data protection system is now ready for use inside the application.
Enforce HTTPS in ASP.NET Core
9/21/2018 • 6 minutes to read • Edit Online
By Rick Anderson
This document shows how to:
Require HTTPS for all requests.
Redirect all HTTP requests to HTTPS.
No API can prevent a client from sending sensitive data on the first request.
WARNING
Do not use RequireHttpsAttribute on Web APIs that receive sensitive information. RequireHttpsAttribute uses HTTP
status codes to redirect browsers from HTTP to HTTPS. API clients may not understand or obey redirects from HTTP to
HTTPS. Such clients may send information over HTTP. Web APIs should either:
Not listen on HTTP.
Close the connection with status code 400 (Bad Request) and not serve the request.
Require HTTPS
We recommend all production ASP.NET Core web apps call:
The HTTPS Redirection Middleware (UseHttpsRedirection) to redirect all HTTP requests to HTTPS.
UseHsts, HTTP Strict Transport Security Protocol (HSTS ).
UseHttpsRedirection
The following code calls UseHttpsRedirection in the Startup class:
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
WARNING
A port must be available for the middleware to redirect to HTTPS. If no port is available, redirection to HTTPS does not
occur. The HTTPS port can be specified by any of the following setting:
HttpsRedirectionOptions.HttpsPort
The ASPNETCORE_HTTPS_PORT environment variable.
In development, an HTTPS url in launchsettings.json.
An HTTPS url configured directly on Kestrel or HttpSys.
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
options.ExcludedHosts.Add("example.com");
options.ExcludedHosts.Add("www.example.com");
});
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
}
NOTE
When an app is run behind a reverse proxy (for example, IIS, IIS Express), IServerAddressesFeature isn't available. The
port must be manually configured. When the port isn't set, requests aren't redirected.
The port can be configured by setting the https_port Web Host configuration setting:
Key: https_port
Type: string
Default: A default value isn't set.
Set using: UseSetting
Environment variable: <PREFIX_>HTTPS_PORT (The prefix is ASPNETCORE_ when using the Web Host.)
WebHost.CreateDefaultBuilder(args)
.UseSetting("https_port", "8080")
NOTE
The port can be configured indirectly by setting the URL with the ASPNETCORE_URLS environment variable. The
environment variable configures the server, and then the middleware indirectly discovers the HTTPS port via
IServerAddressesFeature .
If no port is set:
Requests aren't redirected.
The middleware logs the warning "Failed to determine the https port for redirect."
NOTE
An alternative to using HTTPS Redirection Middleware ( UseHttpsRedirection ) is to use URL Rewriting Middleware (
AddRedirectToHttps ). AddRedirectToHttps can also set the status code and port when the redirect is executed. For
more information, see URL Rewriting Middleware.
When redirecting to HTTPS without the requirement for additional redirect rules, we recommend using HTTPS Redirection
Middleware ( UseHttpsRedirection ) described in this topic.
The preceding highlighted code requires all requests use HTTPS ; therefore, HTTP requests are ignored. The
following highlighted code redirects all HTTP requests to HTTPS:
// Requires using Microsoft.AspNetCore.Rewrite;
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseRewriter(options);
For more information, see URL Rewriting Middleware. The middleware also permits the app to set the status
code or the status code and the port when the redirect is executed.
Requiring HTTPS globally ( options.Filters.Add(new RequireHttpsAttribute()); ) is a security best practice.
Applying the [RequireHttps] attribute to all controllers/Razor Pages isn't considered as secure as requiring
HTTPS globally. You can't guarantee the [RequireHttps] attribute is applied when new controllers and Razor
Pages are added.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
}
UseHsts isn't recommended in development because the HSTS settings are highly cacheable by browsers. By
default, UseHsts excludes the local loopback address.
For production environments implementing HTTPS for the first time, set the initial HSTS value to a small value.
Set the value from hours to no more than a single day in case you need to revert the HTTPS infrastructure to
HTTP. After you're confident in the sustainability of the HTTPS configuration, increase the HSTS max-age value;
a commonly used value is one year.
The following code:
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
options.ExcludedHosts.Add("example.com");
options.ExcludedHosts.Add("www.example.com");
});
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 5001;
});
}
Sets the preload parameter of the Strict-Transport-Security header. Preload isn't part of the RFC HSTS
specification, but is supported by web browsers to preload HSTS sites on fresh install. See
https://hstspreload.org/ for more information.
Enables includeSubDomain, which applies the HSTS policy to Host subdomains.
Explicitly sets the max-age parameter of the Strict-Transport-Security header to 60 days. If not set, defaults to
30 days. See the max-age directive for more information.
Adds example.com to the list of hosts to exclude.
Additional information
OWASP HSTS browser support
EU General Data Protection Regulation (GDPR)
support in ASP.NET Core
9/20/2018 • 5 minutes to read • Edit Online
By Rick Anderson
ASP.NET Core provides APIs and templates to help meet some of the EU General Data Protection Regulation
(GDPR ) requirements:
The project templates include extension points and stubbed markup that you can replace with your privacy
and cookie use policy.
A cookie consent feature allows you to ask for (and track) consent from your users for storing personal
information. If a user hasn't consented to data collection and the app has CheckConsentNeeded set to true ,
non-essential cookies aren't sent to the browser.
Cookies can be marked as essential. Essential cookies are sent to the browser even when the user hasn't
consented and tracking is disabled.
TempData and Session cookies aren't functional when tracking is disabled.
The Identity manage page provides a link to download and delete user data.
The sample app allows you test most of the GDPR extension points and APIs added to the ASP.NET Core 2.1
templates. See the ReadMe file for testing instructions.
View or download sample code (how to download)
// This method gets called by the runtime. Use this method to add services
// to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies
// is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the
// HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
// app.UseSession();
app.UseMvc();
}
}
// This method gets called by the runtime. Use this method to add services
// to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies
// is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the
// HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
// app.UseSession();
app.UseMvc();
}
}
@{
var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
var showBanner = !consentFeature?.CanTrack ?? false;
var cookieString = consentFeature?.CreateConsentCookie();
}
@if (showBanner)
{
<nav id="cookieConsent" class="navbar navbar-default navbar-fixed-top" role="alert">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-
target="#cookieConsent .navbar-collapse">
<span class="sr-only">Toggle cookie consent banner</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span class="navbar-brand"><span class="glyphicon glyphicon-info-sign" aria-hidden="true">
</span></span>
</div>
<div class="collapse navbar-collapse">
<p class="navbar-text">
Use this space to summarize your privacy and cookie use policy.
</p>
<div class="navbar-right">
<a asp-page="/Privacy" class="btn btn-info navbar-btn">Learn More</a>
<button type="button" class="btn btn-default navbar-btn" data-cookie-
string="@cookieString">Accept</button>
</div>
</div>
</div>
</nav>
<script>
(function () {
document.querySelector("#cookieConsent button[data-cookie-string]").addEventListener("click",
function (el) {
document.cookie = el.target.dataset.cookieString;
document.querySelector("#cookieConsent").classList.add("hidden");
}, false);
})();
</script>
}
This partial:
Obtains the state of tracking for the user. If the app is configured to require consent, the user must consent
before cookies can be tracked. If consent is required, the cookie consent panel is fixed at top of the
navigation bar created by the _Layout.cshtml file.
Provides an HTML <p> element to summarize your privacy and cookie use policy.
Provides a link to Privacy page or view where you can detail your site's privacy policy.
Essential cookies
If consent has not been given, only cookies marked essential are sent to the browser. The following code makes
a cookie essential:
public IActionResult OnPostCreateEssentialAsync()
{
HttpContext.Response.Cookies.Append(Constants.EssentialSec,
DateTime.Now.Second.ToString(),
new CookieOptions() { IsEssential = true });
ResponseCookies = Response.Headers[HeaderNames.SetCookie].ToString();
return RedirectToPage("./Index");
}
Session state cookies are not essential. Session state isn't functional when tracking is disabled.
Personal data
ASP.NET Core apps created with individual user accounts include code to download and delete personal data.
Select the user name and then select Personal data:
Notes:
To generate the Account/Manage code, see Scaffold Identity.
Delete and download only impact the default identity data. Apps that create custom user data must be
extended to delete/download the custom user data. For more information, see Add, download, and delete
custom user data to Identity.
Saved tokens for the user that are stored in the Identity database table AspNetUserTokens are deleted when
the user is deleted via the cascading delete behavior due to the foreign key.
Encryption at rest
Some databases and storage mechanisms allow for encryption at rest. Encryption at rest:
Encrypts stored data automatically.
Encrypts without configuration, programming, or other work for the software that accesses the data.
Is the easiest and safest option.
Allows the database to manage keys and encryption.
For example:
Microsoft SQL and Azure SQL provide Transparent Data Encryption (TDE ).
SQL Azure encrypts the database by default
Azure Blobs, Files, Table, and Queue Storage are encrypted by default.
For databases that don't provide built-in encryption at rest, you may be able to use disk encryption to provide
the same protection. For example:
BitLocker for Windows Server
Linux:
eCryptfs
EncFS.
Additional resources
Microsoft.com/GDPR
Safe storage of app secrets in development in
ASP.NET Core
9/24/2018 • 9 minutes to read • Edit Online
Environment variables
Environment variables are used to avoid storage of app secrets in code or in local configuration files.
Environment variables override configuration values for all previously specified configuration sources.
Configure the reading of environment variable values by calling AddEnvironmentVariables in the Startup
constructor:
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
Consider an ASP.NET Core web app in which Individual User Accounts security is enabled. A default
database connection string is included in the project's appsettings.json file with the key DefaultConnection .
The default connection string is for LocalDB, which runs in user mode and doesn't require a password.
During app deployment, the DefaultConnection key value can be overridden with an environment
variable's value. The environment variable may store the complete connection string with sensitive
credentials.
WARNING
Environment variables are generally stored in plain, unencrypted text. If the machine or process is compromised,
environment variables can be accessed by untrusted parties. Additional measures to prevent disclosure of user
secrets may be required.
Secret Manager
The Secret Manager tool stores sensitive data during the development of an ASP.NET Core project. In this
context, a piece of sensitive data is an app secret. App secrets are stored in a separate location from the
project tree. The app secrets are associated with a specific project or shared across several projects. The app
secrets aren't checked into source control.
WARNING
The Secret Manager tool doesn't encrypt the stored secrets and shouldn't be treated as a trusted store. It's for
development purposes only. The keys and values are stored in a JSON configuration file in the user profile directory.
In the preceding file paths, replace <user_secrets_id> with the UserSecretsId value specified in the .csproj
file.
Don't write code that depends on the location or format of data saved with the Secret Manager tool. These
implementation details may change. For example, the secret values aren't encrypted, but could be in the
future.
TIP
Run dotnet --version from a command shell to see the installed .NET Core SDK version number.
A warning is displayed if the .NET Core SDK being used includes the tool:
The tool 'Microsoft.Extensions.SecretManager.Tools' is now included in the .NET Core SDK. Information
on resolving this warning is available at (https://aka.ms/dotnetclitools-in-box).
Install the Microsoft.Extensions.SecretManager.Tools NuGet package in your ASP.NET Core project. For
example:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<UserSecretsId>1242d6d6-9df3-4031-b031-d9b27d13c25a</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore"
Version="1.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets"
Version="1.1.2" />
<PackageReference Include="System.Data.SqlClient"
Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools"
Version="1.0.1" />
</ItemGroup>
</Project>
Execute the following command in a command shell to validate the tool installation:
dotnet user-secrets -h
The Secret Manager tool displays sample usage, options, and command help:
Options:
-?|-h|--help Show help information
--version Show version information
-v|--verbose Show verbose output
-p|--project <PROJECT> Path to project. Defaults to searching the current directory.
-c|--configuration <CONFIGURATION> The project configuration to use. Defaults to 'Debug'.
--id The user secret ID to use.
Commands:
clear Deletes all the application secrets
list Lists all the application secrets
remove Removes the specified user secret
set Sets the user secret to the specified value
Use "dotnet user-secrets [command] --help" for more information about a command.
NOTE
You must be in the same directory as the .csproj file to run tools defined in the .csproj file's
DotNetCliToolReference elements.
Set a secret
The Secret Manager tool operates on project-specific configuration settings stored in your user profile. To
use user secrets, define a UserSecretsId element within a PropertyGroup of the .csproj file. The value of
UserSecretsId is arbitrary, but is unique to the project. Developers typically generate a GUID for the
UserSecretsId .
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<UserSecretsId>1242d6d6-9df3-4031-b031-d9b27d13c25a</UserSecretsId>
</PropertyGroup>
TIP
In Visual Studio, right-click the project in Solution Explorer, and select Manage User Secrets from the context
menu. This gesture adds a UserSecretsId element, populated with a GUID, to the .csproj file. Visual Studio opens
a secrets.json file in the text editor. Replace the contents of secrets.json with the key-value pairs to be stored. For
example:
{
"Movies": {
"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true",
"ServiceApiKey": "12345"
}
}
The JSON structure is flattened after modifications via dotnet user-secrets remove or
dotnet user-secrets set . For example, running dotnet user-secrets remove "Movies:ConnectionString"
collapses the Movies object literal. The modified file resembles the following:
{
"Movies:ServiceApiKey": "12345"
}
Define an app secret consisting of a key and its value. The secret is associated with the project's
UserSecretsId value. For example, run the following command from the directory in which the .csproj file
exists:
In the preceding example, the colon denotes that Movies is an object literal with a ServiceApiKey property.
The Secret Manager tool can be used from other directories too. Use the --project option to supply the
file system path at which the .csproj file exists. For example:
Access a secret
The ASP.NET Core Configuration API provides access to Secret Manager secrets. If your project targets
the .NET Framework, install the Microsoft.Extensions.Configuration.UserSecrets NuGet package.
In ASP.NET Core 2.0 or later, the user secrets configuration source is automatically added in development
mode when the project calls CreateDefaultBuilder to initialize a new instance of the host with
preconfigured defaults. CreateDefaultBuilder calls AddUserSecrets when the EnvironmentName is
Development:
When CreateDefaultBuilder isn't called during host construction, add the user secrets configuration source
with a call to AddUserSecrets in the Startup constructor:
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
The ASP.NET Core Configuration API provides access to Secret Manager secrets. Install the
Microsoft.Extensions.Configuration.UserSecrets NuGet package.
Add the user secrets configuration source with a call to AddUserSecrets in the Startup constructor:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json",
optional: false,
reloadOnChange: true)
.AddEnvironmentVariables();
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
{
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}
To map the preceding secrets to a POCO, use the Configuration API's object graph binding feature. The
following code binds to a custom MovieSettings POCO and accesses the ServiceApiKey property value:
The Movies:ConnectionString and Movies:ServiceApiKey secrets are mapped to the respective properties in
MovieSettings :
{
"ConnectionStrings": {
"Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User
Id=johndoe;Password=pass123;MultipleActiveResultSets=true"
}
}
Remove the Password key-value pair from the connection string in appsettings.json. For example:
{
"ConnectionStrings": {
"Movies": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;User
Id=johndoe;MultipleActiveResultSets=true"
}
}
The secret's value can be set on a SqlConnectionStringBuilder object's Password property to complete the
connection string:
public class Startup
{
private string _connection = null;
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
{
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}
Run the following command from the directory in which the .csproj file exists:
Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true
Movies:ServiceApiKey = 12345
In the preceding example, a colon in the key names denotes the object hierarchy within secrets.json.
{
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}
Run the following command from the directory in which the .csproj file exists:
The app's secrets.json file was modified to remove the key-value pair associated with the
MoviesConnectionString key:
{
"Movies": {
"ServiceApiKey": "12345"
}
}
Movies:ServiceApiKey = 12345
{
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-
1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}
Run the following command from the directory in which the .csproj file exists:
All user secrets for the app have been deleted from the secrets.json file:
{}
Package
To use the provider, add a reference to the Microsoft.Extensions.Configuration.AzureKeyVault package.
App configuration
You can explore the provider with the sample apps. Once you establish a key vault and create secrets in the
vault, the sample apps securely load the secret values into their configurations and display them in webpages.
The provider is added to the app's configuration with the AddAzureKeyVault extension. In the sample apps, the
extension uses three configuration values loaded from the appsettings.json file.
config.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"]);
})
.UseStartup<Startup>();
When you run the app, a webpage shows the loaded secret values:
Create prefixed key vault secrets and load configuration values (key-
name-prefix-sample)
AddAzureKeyVault also provides an overload that accepts an implementation of IKeyVaultSecretManager , which
allows you to control how key vault secrets are converted into configuration keys. For example, you can
implement the interface to load secret values based on a prefix value you provide at app startup. This allows
you, for example, to load secrets based on the version of the app.
WARNING
Don't use prefixes on key vault secrets to place secrets for multiple apps into the same key vault or to place
environmental secrets (for example, development versus production secrets) into the same vault. We recommend that
different apps and development/production environments use separate key vaults to isolate app environments for the
highest level of security.
Using the second sample app, you create a secret in the key vault for 5000-AppSecret (periods aren't allowed in
key vault secret names) representing an app secret for version 5.0.0.0 of your app. For another version, 5.1.0.0,
you create a secret for 5100-AppSecret . Each app version loads its own secret value into its configuration as
AppSecret , stripping off the version as it loads the secret. The sample's implementation is shown below:
config.AddAzureKeyVault(
$"https://{builtConfig["Vault"]}.vault.azure.net/",
builtConfig["ClientId"],
builtConfig["ClientSecret"],
new PrefixKeyVaultSecretManager(versionPrefix));
})
.UseStartup<Startup>();
NOTE
You can also provide your own KeyVaultClient implementation to AddAzureKeyVault . Supplying a custom client
allows you to share a single instance of the client between the configuration provider and other parts of your app.
1. Create a key vault and set up Azure Active Directory (Azure AD ) for the app following the guidance in
Get started with Azure Key Vault.
Add secrets to the key vault using the AzureRM Key Vault PowerShell Module available from the
PowerShell Gallery, the Azure Key Vault REST API, or the Azure Portal. Secrets are created as either
Manual or Certificate secrets. Certificate secrets are certificates for use by apps and services but are
not supported by the configuration provider. You should use the Manual option to create name-value
pair secrets for use with the configuration provider.
Hierarchical values (configuration sections) use -- (two dashes) as a separator.
Create two Manual secrets with the following name-value pairs:
5000-AppSecret : 5.0.0.0_secret_value
5100-AppSecret : 5.1.0.0_secret_value
Register the sample app with Azure Active Directory.
Authorize the app to access the key vault. When you use the Set-AzureRmKeyVaultAccessPolicy
PowerShell cmdlet to authorize the app to access the key vault, provide List and Get access to
secrets with -PermissionsToSecrets list,get .
2. Update the app's appsettings.json file with the values of Vault , ClientId , and ClientSecret .
3. Run the sample app, which obtains its configuration values from IConfigurationRoot with the same
name as the prefixed secret name. In this sample, the prefix is the app's version, which you provided to
the PrefixKeyVaultSecretManager when you added the Azure Key Vault configuration provider. The value
for AppSecret is obtained with config["AppSecret"] . The webpage generated by the app shows the
loaded value:
4. Change the version of the app assembly in the project file from 5.0.0.0 to 5.1.0.0 and run the app
again. This time, the secret value returned is 5.1.0.0_secret_value . The webpage generated by the app
shows the loaded value:
Control access to the ClientSecret
Use the Secret Manager tool to maintain the ClientSecret outside of your project source tree. With Secret
Manager, you associate app secrets with a specific project and share them across multiple projects.
When developing a .NET Framework app in an environment that supports certificates, you can authenticate to
Azure Key Vault with an X.509 certificate. The X.509 certificate's private key is managed by the OS. For more
information, see Authenticate with a Certificate instead of a Client Secret. Use the AddAzureKeyVault overload
that accepts an X509Certificate2 ( _env in the following example :
config.AddAzureKeyVault(
builtConfig["Vault"],
builtConfig["ClientId"],
cert.OfType<X509Certificate2>().Single(),
new EnvironmentSecretManager(context.HostingEnvironment.ApplicationName));
store.Close();
Reload secrets
Secrets are cached until IConfigurationRoot.Reload() is called. Expired, disabled, and updated secrets in the key
vault are not respected by the app until Reload is executed.
Configuration.Reload();
Troubleshoot
When the app fails to load configuration using the provider, an error message is written to the ASP.NET Core
Logging infrastructure. The following conditions will prevent configuration from loading:
The app isn't configured correctly in Azure Active Directory.
The key vault doesn't exist in Azure Key Vault.
The app isn't authorized to access the key vault.
The access policy doesn't include Get and List permissions.
In the key vault, the configuration data (name-value pair) is incorrectly named, missing, disabled, or expired.
The app has the wrong key vault name ( Vault ), Azure AD App Id ( ClientId ), or Azure AD Key (
ClientSecret ).
The Azure AD Key ( ClientSecret ) is expired.
The configuration key (name) is incorrect in the app for the value you're trying to load.
Additional resources
Configuration in ASP.NET Core
Microsoft Azure: Key Vault
Microsoft Azure: Key Vault Documentation
How to generate and transfer HSM -protected keys for Azure Key Vault
KeyVaultClient Class
Prevent Cross-Site Request Forgery (XSRF/CSRF)
attacks in ASP.NET Core
7/16/2018 • 13 minutes to read • Edit Online
Notice that the form's action posts to the vulnerable site, not to the malicious site. This is the
"cross-site" part of CSRF.
3. The user selects the submit button. The browser makes the request and automatically includes the
authentication cookie for the requested domain, www.good-banking-site.com .
4. The request runs on the www.good-banking-site.com server with the user's authentication context
and can perform any action that an authenticated user is allowed to perform.
In addition to the scenario where the user selects the button to submit the form, the malicious site could:
Run a script that automatically submits the form.
Send the form submission as an AJAX request.
Hide the form using CSS.
These alternative scenarios don't require any action or input from the user other than initially visiting the
malicious site.
Using HTTPS doesn't prevent a CSRF attack. The malicious site can send an
https://www.good-banking-site.com/ request just as easily as it can send an insecure request.
Some attacks target endpoints that respond to GET requests, in which case an image tag can be used to
perform the action. This form of attack is common on forum sites that permit images but block JavaScript.
Apps that change state on GET requests, where variables or resources are altered, are vulnerable to
malicious attacks. GET requests that change state are insecure. A best practice is to never change
state on a GET request.
CSRF attacks are possible against web apps that use cookies for authentication because:
Browsers store cookies issued by a web app.
Stored cookies include session cookies for authenticated users.
Browsers send all of the cookies associated with a domain to the web app every request regardless of
how the request to app was generated within the browser.
However, CSRF attacks aren't limited to exploiting cookies. For example, Basic and Digest authentication
are also vulnerable. After a user signs in with Basic or Digest authentication, the browser automatically
sends the credentials until the session† ends.
†In this context, session refers to the client-side session during which the user is authenticated. It's
unrelated to server-side sessions or ASP.NET Core Session Middleware.
Users can guard against CSRF vulnerabilities by taking precautions:
Sign off of web apps when finished using them.
Clear browser cookies periodically.
However, CSRF vulnerabilities are fundamentally a problem with the web app, not the end user.
Authentication fundamentals
Cookie-based authentication is a popular form of authentication. Token-based authentication systems are
growing in popularity, especially for Single Page Applications (SPAs).
Cookie -based authentication
When a user authenticates using their username and password, they're issued a token, containing an
authentication ticket that can be used for authentication and authorization. The token is stored as a cookie
that accompanies every request the client makes. Generating and validating this cookie is performed by
the Cookie Authentication Middleware. The middleware serializes a user principal into an encrypted
cookie. On subsequent requests, the middleware validates the cookie, recreates the principal, and assigns
the principal to the User property of HttpContext.
Token-based authentication
When a user is authenticated, they're issued a token (not an antiforgery token). The token contains user
information in the form of claims or a reference token that points the app to user state maintained in the
app. When a user attempts to access a resource requiring authentication, the token is sent to the app with
an additional authorization header in form of Bearer token. This makes the app stateless. In each
subsequent request, the token is passed in the request for server-side validation. This token isn't
encrypted; it's encoded. On the server, the token is decoded to access its information. To send the token on
subsequent requests, store the token in the browser's local storage. Don't be concerned about CSRF
vulnerability if the token is stored in the browser's local storage. CSRF is a concern when the token is
stored in a cookie.
Multiple apps hosted at one domain
Shared hosting environments are vulnerable to session hijacking, login CSRF, and other attacks.
Although example1.contoso.net and example2.contoso.net are different hosts, there's an implicit trust
relationship between hosts under the *.contoso.net domain. This implicit trust relationship allows
potentially untrusted hosts to affect each other's cookies (the same-origin policies that govern AJAX
requests don't necessarily apply to HTTP cookies).
Attacks that exploit trusted cookies between apps hosted on the same domain can be prevented by not
sharing domains. When each app is hosted on its own domain, there is no implicit cookie trust relationship
to exploit.
In ASP.NET Core 2.0 or later, the FormTagHelper injects antiforgery tokens into HTML form elements.
The following markup in a Razor file automatically generates antiforgery tokens:
<form method="post">
...
</form>
Similarily, IHtmlHelper.BeginForm generates antiforgery tokens by default if the form's method isn't GET.
The automatic generation of antiforgery tokens for HTML form elements happens when the <form> tag
contains the method="post" attribute and either of the following are true:
The action attribute is empty ( action="" ).
The action attribute isn't supplied ( <form method="post"> ).
Automatic generation of antiforgery tokens for HTML form elements can be disabled:
Explicitly disable antiforgery tokens with the asp-antiforgery attribute:
The form element is opted-out of Tag Helpers by using the Tag Helper ! opt-out symbol:
<!form method="post">
...
</!form>
Remove the FormTagHelper from the view. The FormTagHelper can be removed from a view by
adding the following directive to the Razor view:
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper,
Microsoft.AspNetCore.Mvc.TagHelpers
NOTE
Razor Pages are automatically protected from XSRF/CSRF. For more information, see XSRF/CSRF and Razor Pages.
The most common approach to defending against CSRF attacks is to use the Synchronizer Token Pattern
(STP ). STP is used when the user requests a page with form data:
1. The server sends a token associated with the current user's identity to the client.
2. The client sends back the token to the server for verification.
3. If the server receives a token that doesn't match the authenticated user's identity, the request is
rejected.
The token is unique and unpredictable. The token can also be used to ensure proper sequencing of a series
of requests (for example, ensuring the request sequence of: page 1 – page 2 – page 3). All of the forms in
ASP.NET Core MVC and Razor Pages templates generate antiforgery tokens. The following pair of view
examples generate antiforgery tokens:
Explicitly add an antiforgery token to a <form> element without using Tag Helpers with the HTML helper
@Html.AntiForgeryToken:
In each of the preceding cases, ASP.NET Core adds a hidden form field similar to the following:
ASP.NET Core includes three filters for working with antiforgery tokens:
ValidateAntiForgeryToken
AutoValidateAntiforgeryToken
IgnoreAntiforgeryToken
Antiforgery options
Customize antiforgery options in Startup.ConfigureServices :
services.AddAntiforgery(options =>
{
options.CookieDomain = "contoso.com";
options.CookieName = "X-CSRF-TOKEN-COOKIENAME";
options.CookiePath = "Path";
options.FormFieldName = "AntiforgeryFieldname";
options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
options.RequireSsl = false;
options.SuppressXFrameOptionsHeader = false;
});
OPTION DESCRIPTION
CookieName The name of the cookie. If not set, the system generates
a unique name beginning with the DefaultCookiePrefix
(".AspNetCore.Antiforgery."). This property is obsolete
and will be removed in a future version. The
recommended alternative is Cookie.Name.
CookiePath The path set on the cookie. This property is obsolete and
will be removed in a future version. The recommended
alternative is Cookie.Path.
if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// The request token can be sent as a JavaScript-readable cookie,
// and Angular uses it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel account)
{
ManageMessageId? message = ManageMessageId.Error;
var user = await GetCurrentUserAsync();
if (user != null)
{
var result =
await _userManager.RemoveLoginAsync(
user, account.LoginProvider, account.ProviderKey);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
message = ManageMessageId.RemoveLoginSuccess;
}
}
The ValidateAntiForgeryToken attribute requires a token for requests to the action methods it decorates,
including HTTP GET requests. If the ValidateAntiForgeryToken attribute is applied across the app's
controllers, it can be overridden with the IgnoreAntiforgeryToken attribute.
NOTE
ASP.NET Core doesn't support adding antiforgery tokens to GET requests automatically.
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
Global example:
services.AddMvc(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
[HttpPost]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> DoSomethingSafe(SomeViewModel model)
{
// no antiforgery token required
}
}
@{
ViewData["Title"] = "AJAX Demo";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
public string GetAntiXsrfRequestToken()
{
return Xsrf.GetAndStoreTokens(Context).RequestToken;
}
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
<div class="row">
<p><input type="button" id="antiforgery" value="Antiforgery"></p>
<script>
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == XMLHttpRequest.DONE) {
if (xhttp.status == 200) {
alert(xhttp.responseText);
} else {
alert('There was an error processing the AJAX request.');
}
}
};
document.addEventListener('DOMContentLoaded', function() {
document.getElementById("antiforgery").onclick = function () {
xhttp.open('POST', '@Url.Action("Antiforgery", "Home")', true);
xhttp.setRequestHeader("RequestVerificationToken",
document.getElementById('RequestVerificationToken').value);
xhttp.send();
}
});
</script>
</div>
This approach eliminates the need to deal directly with setting cookies from the server or reading them
from the client.
The preceding example uses JavaScript to read the hidden field value for the AJAX POST header.
JavaScript can also access tokens in cookies and use the cookie's contents to create a header with the
token's value.
context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken,
new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
Assuming the script requests to send the token in a header called X-CSRF-TOKEN , configure the antiforgery
service to look for the X-CSRF-TOKEN header:
The following example uses JavaScript to make an AJAX request with the appropriate header:
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
AngularJS
AngularJS uses a convention to address CSRF. If the server sends a cookie with the name XSRF-TOKEN , the
AngularJS $http service adds the cookie value to a header when it sends a request to the server. This
process is automatic. The header doesn't need to be set explicitly. The header name is X-XSRF-TOKEN . The
server should detect this header and validate its contents.
For ASP.NET Core API work with this convention:
Configure your app to provide a token in a cookie called XSRF-TOKEN .
Configure the antiforgery service to look for a header named X-XSRF-TOKEN .
Additional resources
CSRF on Open Web Application Security Project (OWASP ).
Host ASP.NET Core in a web farm
Prevent open redirect attacks in ASP.NET Core
8/16/2018 • 3 minutes to read • Edit Online
A web app that redirects to a URL that's specified via the request such as the querystring or form data can
potentially be tampered with to redirect users to an external, malicious URL. This tampering is called an open
redirection attack.
Whenever your application logic redirects to a specified URL, you must verify that the redirection URL hasn't been
tampered with. ASP.NET Core has built-in functionality to help protect apps from open redirect (also known as
open redirection) attacks.
LocalRedirect will throw an exception if a non-local URL is specified. Otherwise, it behaves just like the Redirect
method.
IsLocalUrl
Use the IsLocalUrl method to test URLs before redirecting:
The following example shows how to check whether a URL is local before redirecting.
The IsLocalUrl method protects users from being inadvertently redirected to a malicious site. You can log the
details of the URL that was provided when a non-local URL is supplied in a situation where you expected a local
URL. Logging redirect URLs may help in diagnosing redirection attacks.
Prevent Cross-Site Scripting (XSS) in ASP.NET Core
7/30/2018 • 6 minutes to read • Edit Online
By Rick Anderson
Cross-Site Scripting (XSS ) is a security vulnerability which enables an attacker to place client side scripts (usually
JavaScript) into web pages. When other users load affected pages the attackers scripts will run, enabling the
attacker to steal cookies and session tokens, change the contents of the web page through DOM manipulation or
redirect the browser to another page. XSS vulnerabilities generally occur when an application takes user input and
outputs it in a page without validating, encoding or escaping it.
@{
var untrustedInput = "<\"123\">";
}
@untrustedInput
This view outputs the contents of the untrustedInput variable. This variable includes some characters which are
used in XSS attacks, namely <, " and >. Examining the source shows the rendered output encoded as:
<"123">
WARNING
ASP.NET Core MVC provides an HtmlString class which isn't automatically encoded upon output. This should never be
used in combination with untrusted input as this will expose an XSS vulnerability.
@{
var untrustedInput = "<\"123\">";
}
<div
id="injectedData"
data-untrustedinput="@untrustedInput" />
<script>
var injectedData = document.getElementById("injectedData");
// All clients
var clientSideUntrustedInputOldStyle =
injectedData.getAttribute("data-untrustedinput");
document.write(clientSideUntrustedInputOldStyle);
document.write("<br />")
document.write(clientSideUntrustedInputHtml5);
</script>
<div
id="injectedData"
data-untrustedinput="<"123">" />
<script>
var injectedData = document.getElementById("injectedData");
var clientSideUntrustedInputOldStyle =
injectedData.getAttribute("data-untrustedinput");
var clientSideUntrustedInputHtml5 =
injectedData.dataset.untrustedinput;
document.write(clientSideUntrustedInputOldStyle);
document.write("<br />")
document.write(clientSideUntrustedInputHtml5);
</script>
Which, when it runs, will render the following;
<"123">
<"123">
@using System.Text.Encodings.Web;
@inject JavaScriptEncoder encoder;
@{
var untrustedInput = "<\"123\">";
}
<script>
document.write("@encoder.Encode(untrustedInput)");
</script>
<script>
document.write("\u003C\u0022123\u0022\u003E");
</script>
WARNING
Don't concatenate untrusted input in JavaScript to create DOM elements. You should use createElement() and assign
property values appropriately such as node.TextContent= , or use element.SetAttribute() / element[attribute]=
otherwise you expose yourself to DOM-based XSS.
WARNING
Don't use untrusted input as part of a URL path. Always pass untrusted input as a query string value.
When you view the source of the web page you will see it has been rendered as follows, with the Chinese text
encoded;
To widen the characters treated as safe by the encoder you would insert the following line into the
ConfigureServices() method in startup.cs ;
services.AddSingleton<HtmlEncoder>(
HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
UnicodeRanges.CjkUnifiedIdeographs }));
This example widens the safe list to include the Unicode Range CjkUnifiedIdeographs. The rendered output would
now become
Safe list ranges are specified as Unicode code charts, not languages. The Unicode standard has a list of code
charts you can use to find the chart containing your characters. Each encoder, Html, JavaScript and Url, must be
configured separately.
NOTE
Customization of the safe list only affects encoders sourced via DI. If you directly access an encoder via
System.Text.Encodings.Web.*Encoder.Default then the default, Basic Latin only safelist will be used.
Same origin
Two URLs have the same origin if they have identical schemes, hosts, and ports ( RFC 6454).
These two URLs have the same origin:
https://example.com/foo.html
https://example.com/bar.html
These URLs have different origins than the previous two URLs:
https://example.net – Different domain
https://www.example.com/foo.html – Different subdomain
http://example.com/foo.html – Different scheme
https://example.com:9000/foo.html – Different port
NOTE
Internet Explorer doesn't consider the port when comparing origins.
Enable CORS
After registering CORS services, use either of the following approaches to enable CORS in an ASP.NET Core
app:
CORS Middleware – Apply CORS policies globally to the app via middleware.
CORS in MVC – Apply CORS policies per action or per controller. CORS Middleware isn't used.
Enable CORS with CORS Middleware
CORS Middleware handles cross-origin requests to the app. To enable CORS Middleware in the request
processing pipeline, call the UseCors extension method in Startup.Configure .
CORS Middleware must precede any defined endpoints in your app where you want to support cross-origin
requests (for example, before the call to UseMvc for MVC/Razor Pages Middleware).
A cross-origin policy can be specified when adding the CORS Middleware using the CorsPolicyBuilder class.
There are two approaches for defining a CORS policy:
Call UseCors with a lambda:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
The lambda takes a CorsPolicyBuilder object. Configuration options, such as WithOrigins , are described
later in this topic. In the preceding example, the policy allows cross-origin requests from
https://example.com and no other origins.
The URL must be specified without a trailing slash ( / ). If the URL terminates with / , the comparison
returns false and no header is returned.
CorsPolicyBuilder has a fluent API, so you can chain method calls:
app.UseCors(builder =>
builder.WithOrigins("http://example.com")
.AllowAnyHeader()
);
Define one or more named CORS policies and select the policy by name at runtime. The following
example adds a user-defined CORS policy named AllowSpecificOrigin. To select the policy, pass the name
to UseCors :
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
[HttpGet]
[EnableCors("AllowSpecificOrigin")]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Per controller
To specify the CORS policy for a specific controller, add the [EnableCors] attribute to the controller class. Specify
the policy name.
[Route("api/[controller]")]
[EnableCors("AllowSpecificOrigin")]
public class ValuesController : ControllerBase
[HttpGet("{id}")]
[DisableCors]
public string Get(int id)
{
return "value";
}
options.AddPolicy("AllowSpecificOrigins",
builder =>
{
builder.WithOrigins("http://example.com", "http://www.contoso.com");
});
options.AddPolicy("AllowAllOrigins",
builder =>
{
builder.AllowAnyOrigin();
});
Consider carefully before allowing requests from any origin. Allowing requests from any origin means that any
website can make cross-origin requests to your app.
This setting affects preflight requests and the Access-Control-Allow -Origin header (described later in this topic).
Set the allowed HTTP methods
To allow all HTTP methods, call AllowAnyMethod:
options.AddPolicy("AllowAllMethods",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyMethod();
});
This setting affects preflight requests and the Access-Control-Allow -Methods header (described later in this
topic).
Set the allowed request headers
To allow specific headers to be sent in a CORS request, called author request headers, call WithHeaders and
specify the allowed headers:
options.AddPolicy("AllowHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithHeaders(HeaderNames.ContentType, "x-custom-header");
});
options.AddPolicy("AllowAllHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyHeader();
});
This setting affects preflight requests and the Access-Control-Request-Headers header (described later in this
topic).
A CORS Middleware policy match to specific headers specified by WithHeaders is only possible when the
headers sent in Access-Control-Request-Headers exactly match the headers stated in WithHeaders .
For instance, consider an app configured as follows:
CORS Middleware declines a preflight request with the following request header because Content-Language
(HeaderNames.ContentLanguage) isn't listed in WithHeaders :
The app returns a 200 OK response but doesn't send the CORS headers back. Therefore, the browser doesn't
attempt the cross-origin request.
CORS Middleware always allows four headers in the Access-Control-Request-Headers to be sent regardless of
the values configured in CorsPolicy.Headers. This list of headers includes:
Accept
Accept-Language
Content-Language
Origin
CORS Middleware responds successfully to a preflight request with the following request header because
Content-Language is always whitelisted:
The CORS specification calls these headers simple response headers. To make other headers available to the app,
call WithExposedHeaders:
options.AddPolicy("ExposeResponseHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithExposedHeaders("x-custom-header");
});
In jQuery:
$.ajax({
type: 'get',
url: 'https://www.example.com/home',
xhrFields: {
withCredentials: true
}
In addition, the server must allow the credentials. To allow cross-origin credentials, call AllowCredentials:
options.AddPolicy("AllowCredentials",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowCredentials();
});
The HTTP response includes an Access-Control-Allow-Credentials header, which tells the browser that the
server allows credentials for a cross-origin request.
If the browser sends credentials but the response doesn't include a valid Access-Control-Allow-Credentials
header, the browser doesn't expose the response to the app, and the cross-origin request fails.
Be careful when allowing cross-origin credentials. A website at another domain can send a signed-in user's
credentials to the app on the user's behalf without the user's knowledge.
The CORS specification also states that setting origins to "*" (all origins) is invalid if the
Access-Control-Allow-Credentials header is present.
Preflight requests
For some CORS requests, the browser sends an additional request before making the actual request. This
request is called a preflight request. The browser can skip the preflight request if the following conditions are
true:
The request method is GET, HEAD, or POST.
The app doesn't set request headers other than Accept , Accept-Language , Content-Language , Content-Type ,
or Last-Event-ID .
The Content-Type header, if set, has one of the one of the following values:
application/x-www-form-urlencoded
multipart/form-data
text/plain
The rule on request headers set for the client request applies to headers that the app sets by calling
setRequestHeader on the XMLHttpRequest object. The CORS specification calls these headers author request
headers. The rule doesn't apply to headers the browser can set, such as User-Agent , Host , or Content-Length .
The following is an example of a preflight request:
The pre-flight request uses the HTTP OPTIONS method. It includes two special headers:
Access-Control-Request-Method : The HTTP method that will be used for the actual request.
Access-Control-Request-Headers : A list of request headers that the app sets on the actual request. As stated
earlier, this doesn't include headers that the browser sets, such as User-Agent .
A CORS preflight request might include an Access-Control-Request-Headers header, which indicates to the server
the headers that are sent with the actual request.
To allow specific headers, call WithHeaders:
options.AddPolicy("AllowHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.WithHeaders(HeaderNames.ContentType, "x-custom-header");
});
options.AddPolicy("AllowAllHeaders",
builder =>
{
builder.WithOrigins("http://example.com")
.AllowAnyHeader();
});
Browsers aren't entirely consistent in how they set Access-Control-Request-Headers . If you set headers to
anything other than "*" (or use AllowAnyHeader), you should include at least Accept , Content-Type , and
Origin , plus any custom headers that you want to support.
The following is an example response to the preflight request (assuming that the server allows the request):
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: https://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT
Date: Wed, 20 May 2015 06:33:22 GMT
The response includes an Access-Control-Allow-Methods header that lists the allowed methods and optionally an
Access-Control-Allow-Headers header, which lists the allowed headers. If the preflight request succeeds, the
browser sends the actual request.
If the preflight request is denied, the app returns a 200 OK response but doesn't send the CORS headers back.
Therefore, the browser doesn't attempt the cross-origin request.
Set the preflight expiration time
The Access-Control-Max-Age header specifies how long the response to the preflight request can be cached. To
set this header, call SetPreflightMaxAge:
options.AddPolicy("SetPreflightExpiration",
builder =>
{
builder.WithOrigins("http://example.com")
.SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
});
If the server allows the request, it sets the Access-Control-Allow-Origin header in the response. The value of this
header either matches the Origin header from the request or is the wildcard value "*" , meaning that any
origin is allowed:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
Access-Control-Allow-Origin: https://myclient.azurewebsites.net
Date: Wed, 20 May 2015 06:27:30 GMT
Content-Length: 12
Test message
If the response doesn't include the Access-Control-Allow-Origin header, the cross-origin request fails.
Specifically, the browser disallows the request. Even if the server returns a successful response, the browser
doesn't make the response available to the client app.
Additional resources
Cross-Origin Resource Sharing (CORS )
Share cookies among apps with ASP.NET and
ASP.NET Core
8/30/2018 • 5 minutes to read • Edit Online
services.AddDataProtection()
.PersistKeysToFileSystem(GetKeyRingDirInfo())
.SetApplicationName("SharedCookieApp");
services.ConfigureApplicationCookie(options => {
options.Cookie.Name = ".AspNet.SharedCookie";
});
Data protection keys and the app name must be shared among apps. In the sample apps, GetKeyRingDirInfo
returns the common key storage location to the PersistKeysToFileSystem method. Use SetApplicationName to
configure a common shared app name ( SharedCookieApp in the sample). For more information, see Configuring
Data Protection.
When hosting apps that share cookies across subdomains, specify a common domain in the Cookie.Domain
property. To share cookies across apps at contoso.com , such as first_subdomain.contoso.com and
second_subdomain.contoso.com , specify the Cookie.Domain as .contoso.com :
options.Cookie.Domain = ".contoso.com";
var protectionProvider =
DataProtectionProvider.Create(
new DirectoryInfo(@"PATH_TO_KEY_RING_FOLDER"));
options.Cookies.ApplicationCookie.DataProtectionProvider =
protectionProvider;
options.Cookies.ApplicationCookie.TicketDataFormat =
new TicketDataFormat(protectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
"Cookies",
"v2"));
});
services.AddDataProtection()
.PersistKeysToFileSystem(GetKeyRingDirInfo())
.SetApplicationName("SharedCookieApp");
services.AddAuthentication("Identity.Application")
.AddCookie("Identity.Application", options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
});
Data protection keys and the app name must be shared among apps. In the sample apps, GetKeyRingDirInfo
returns the common key storage location to the PersistKeysToFileSystem method. Use SetApplicationName to
configure a common shared app name ( SharedCookieApp in the sample). For more information, see Configuring
Data Protection.
When hosting apps that share cookies across subdomains, specify a common domain in the Cookie.Domain
property. To share cookies across apps at contoso.com , such as first_subdomain.contoso.com and
second_subdomain.contoso.com , specify the Cookie.Domain as .contoso.com :
options.Cookie.Domain = ".contoso.com";
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
DataProtectionProvider =
DataProtectionProvider.Create(
new DirectoryInfo(@"PATH_TO_KEY_RING_FOLDER"))
});
services.AddDataProtection()
.ProtectKeysWithCertificate("thumbprint");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
DataProtectionProvider = DataProtectionProvider.Create(
new DirectoryInfo(@"PATH_TO_KEY_RING"),
configure =>
{
configure.ProtectKeysWithCertificate("thumbprint");
})
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Identity.Application",
CookieName = ".AspNet.SharedCookie",
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity =
SecurityStampValidator
.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) =>
user.GenerateUserIdentityAsync(manager))
},
TicketDataFormat = new AspNetTicketDataFormat(
new DataProtectorShim(
DataProtectionProvider.Create(GetKeyRingDirInfo(),
(builder) => { builder.SetApplicationName("SharedCookieApp"); })
.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
"Identity.Application",
"v2"))),
CookieManager = new ChunkingCookieManager()
});
Additional resources
Host ASP.NET Core in a web farm
Client IP safelist for ASP.NET Core
9/7/2018 • 3 minutes to read • Edit Online
The safelist
The list is configured in the appsettings.json file. It's a semicolon-delimited list and can contain IPv4 and IPv6
addresses.
{
"AdminSafeList": "127.0.0.1;192.168.1.5;::1",
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
Middleware
The Configure method adds the middleware and passes the safelist string to it in a constructor parameter.
app.UseStaticFiles();
app.UseMiddleware<AdminSafeListMiddleware>(
Configuration["AdminSafeList"]);
app.UseMvc();
}
The middleware parses the string into an array and looks for the remote IP address in the array. If the remote IP
address is not found, the middleware returns HTTP 401 Forbidden. This validation process is bypassed for HTTP
Get requests.
public AdminSafeListMiddleware(
RequestDelegate next,
ILogger<AdminSafeListMiddleware> logger,
string adminSafeList)
{
_adminSafeList = adminSafeList;
_next = next;
_logger = logger;
}
string[] ip = _adminSafeList.Split(';');
if(badIp)
{
_logger.LogInformation(
$"Forbidden Request from Remote IP address: {remoteIp}");
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
}
await _next.Invoke(context);
}
}
Action filter
If you want a safelist only for specific controllers or action methods, use an action filter. Here's an example:
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace ClientIpAspNetCore.Filters
{
public class ClientIdCheckFilter : ActionFilterAttribute
{
private readonly ILogger _logger;
private readonly string _safelist;
public ClientIdCheckFilter
(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_logger = loggerFactory.CreateLogger("ClientIdCheckFilter");
_safelist = configuration["AdminSafeList"];
}
string[] ip = _safelist.Split(';');
if (badIp)
{
_logger.LogInformation(
$"Forbidden Request from Remote IP address: {remoteIp}");
context.Result = new StatusCodeResult(401);
return;
}
base.OnActionExecuting(context);
}
}
}
services.AddMvc(options =>
{
options.Filters.Add
(new ClientIdCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
[ServiceFilter(typeof(ClientIdCheckFilter))]
[HttpGet]
public IEnumerable<string> Get()
In the sample app, the filter is applied to the Get method. So when you test the app by sending a Get API
request, the attribute is validating the client IP address. When you test by calling the API with any other HTTP
method, the middleware is validating the client IP.
namespace ClientIpAspNetCore
{
public class ClientIdCheckPageFilter : IPageFilter
{
private readonly ILogger _logger;
private readonly string _safelist;
public ClientIdCheckPageFilter
(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_logger = loggerFactory.CreateLogger("ClientIdCheckPageFilter");
_safelist = configuration["AdminSafeList"];
}
string[] ip = _safelist.Split(';');
if (badIp)
{
_logger.LogInformation(
$"Forbidden Request from Remote IP address: {remoteIp}");
context.Result = new StatusCodeResult(401);
return;
}
}
services.AddMvc(options =>
{
options.Filters.Add
(new ClientIdCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
When you run the app and request a Razor page, the Razor Pages filter is validating the client IP.
Next steps
Learn more about ASP.NET Core Middleware.
Performance in ASP.NET Core
9/24/2018 • 2 minutes to read • Edit Online
Cache in-memory
Learn how to cache data in memory in ASP.NET Core.
Work with a distributed cache
Learn how to use ASP.NET Core distributed caching to improve app performance and scalability, especially in a
cloud or server farm environment.
Response caching
Learn how to use response caching to lower bandwidth requirements and increase performance of ASP.NET Core
apps.
Response Caching Middleware
Learn how to configure and use Response Caching Middleware in ASP.NET Core.
Cache Tag Helper
Learn how to use the Cache Tag Helper.
Distributed Cache Tag Helper
Learn how to use the Distributed Cache Tag Helper.
Cache in-memory in ASP.NET Core
9/20/2018 • 8 minutes to read • Edit Online
Caching basics
Caching can significantly improve the performance and scalability of an app by reducing the work required to
generate content. Caching works best with data that changes infrequently. Caching makes a copy of data that
can be returned much faster than from the original source. You should write and test your app to never depend
on cached data.
ASP.NET Core supports several different caches. The simplest cache is based on the IMemoryCache, which
represents a cache stored in the memory of the web server. Apps which run on a server farm of multiple servers
should ensure that sessions are sticky when using the in-memory cache. Sticky sessions ensure that subsequent
requests from a client all go to the same server. For example, Azure Web apps use Application Request Routing
(ARR ) to route all subsequent requests to the same server.
Non-sticky sessions in a web farm require a distributed cache to avoid cache consistency problems. For some
apps, a distributed cache can support higher scale-out than an in-memory cache. Using a distributed cache
offloads the cache memory to an external process.
The IMemoryCache cache will evict cache entries under memory pressure unless the cache priority is set to
CacheItemPriority.NeverRemove . You can set the CacheItemPriority to adjust the priority with which the cache
evicts items under memory pressure.
The in-memory cache can store any object; the distributed cache interface is limited to byte[] .
System.Runtime.Caching/MemoryCache
System.Runtime.Caching/MemoryCache (NuGet package) can be used with:
.NET Standard 2.0 or later.
Any .NET implementation that targets .NET Standard 2.0 or later. For example, ASP.NET Core 2.0 or later.
.NET Framework 4.5 or later.
Microsoft.Extensions.Caching.Memory/ IMemoryCache (described in this topic) is recommended over
System.Runtime.Caching / MemoryCache because it's better integrated into ASP.NET Core. For example,
IMemoryCache works natively with ASP.NET Core dependency injection.
Use System.Runtime.Caching / MemoryCache as a compatibility bridge when porting code from ASP.NET 4.x to
ASP.NET Core.
Cache guidelines
Code should always have a fallback option to fetch data and not depend on a cached value being available.
The cache uses a scarce resource, memory. Limit cache growth:
Do not use external input as cache keys.
Use expirations to limit cache growth.
Use SetSize, Size, and SizeLimit to limit cache size
Using IMemoryCache
In-memory caching is a service that's referenced from your app using Dependency Injection. Call
AddMemoryCache in ConfigureServices :
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
@model DateTime?
<div>
<h2>Actions</h2>
<ul>
<li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
<li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
<li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
<li><a asp-controller="Home" asp-action="CacheGetOrCreateAsync">GetOrCreateAsync</a></li>
<li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
</ul>
</div>
The cached DateTime value remains in the cache while there are requests within the timeout period (and no
eviction due to memory pressure). The following image shows the current time and an older time retrieved from
the cache:
The following code uses GetOrCreate and GetOrCreateAsync to cache data.
See IMemoryCache methods and CacheExtensions methods for a description of the cache methods.
MemoryCacheEntryOptions
The following sample:
Sets the absolute expiration time. This is the maximum time the entry can be cached and prevents the item
from becoming too stale when the sliding expiration is continuously renewed.
Sets a sliding expiration time. Requests that access this cached item will reset the sliding expiration clock.
Sets the cache priority to CacheItemPriority.NeverRemove .
Sets a PostEvictionDelegate that will be called after the entry is evicted from the cache. The callback is run on
a different thread from the code that removes the item from the cache.
return RedirectToAction("GetCallbackEntry");
}
SizeLimit does not have units. Cached entries must specify size in whatever units they deem most appropriate if
the cache memory size has been set. All users of a cache instance should use the same unit system. An entry will
not be cached if the sum of the cached entry sizes exceeds the value specified by SizeLimit . If no cache size
limit is set, the cache size set on the entry will be ignored.
The following code registers MyMemoryCache with the dependency injection container.
services.AddSingleton<MyMemoryCache>();
}
MyMemoryCache is created as an independent memory cache for components that are aware of this size limited
cache and know how to set cache entry size appropriately.
The following code uses MyMemoryCache :
public class AboutModel : PageModel
{
private MemoryCache _cache;
public static readonly string MyKey = "_MyKey";
[TempData]
public string DateTime_Now { get; set; }
DateTime_Now = cacheEntry;
return RedirectToPage("./Index");
}
}
The size of the cache entry can be set by Size or the SetSize extension method:
public IActionResult OnGet()
{
if (!_cache.TryGetValue(MyKey, out string cacheEntry))
{
// Key not in cache, so get data.
cacheEntry = DateTime.Now.TimeOfDay.ToString();
DateTime_Now = cacheEntry;
return RedirectToPage("./Index");
}
Cache dependencies
The following sample shows how to expire a cache entry if a dependent entry expires. A
CancellationChangeToken is added to the cached item. When Cancel is called on the CancellationTokenSource ,
both cache entries are evicted.
public IActionResult CreateDependentEntries()
{
var cts = new CancellationTokenSource();
_cache.Set(CacheKeys.DependentCTS, cts);
_cache.Set(CacheKeys.Child,
DateTime.Now,
new CancellationChangeToken(cts.Token));
}
return RedirectToAction("GetDependentEntries");
}
Using a CancellationTokenSource allows multiple cache entries to be evicted as a group. With the using pattern
in the code above, cache entries created inside the using block will inherit triggers and expiration settings.
Additional notes
When using a callback to repopulate a cache item:
Multiple requests can find the cached key value empty because the callback hasn't completed.
This can result in several threads repopulating the cached item.
When one cache entry is used to create another, the child copies the parent entry's expiration tokens and
time-based expiration settings. The child isn't expired by manual removal or updating of the parent entry.
Use PostEvictionCallbacks to set the callbacks that will be fired after the cache entry is evicted from the
cache.
Additional resources
Work with a distributed cache
Detect changes with change tokens
Response caching
Response Caching Middleware
Cache Tag Helper
Distributed Cache Tag Helper
Work with a distributed cache in ASP.NET Core
7/18/2018 • 5 minutes to read • Edit Online
By Steve Smith
Distributed caches can improve the performance and scalability of ASP.NET Core apps, especially when hosted
in the cloud or a server farm.
View or download sample code (how to download)
NOTE
If using a SQL Server Distributed Cache, some of these advantages are only true if a separate database instance is used
for the cache than for the app's source data.
Like any cache, a distributed cache can dramatically improve an app's responsiveness, since typically data can
be retrieved from the cache much faster than from a relational database (or web service).
Cache configuration is implementation specific. This article describes how to configure both Redis and SQL
Server distributed caches. Regardless of which implementation is selected, the app interacts with the cache
using a common IDistributedCache interface.
NOTE
There's no need to use a Singleton or Scoped lifetime for IDistributedCache instances (at least for the built-in
implementations). You can also create an instance wherever you might need one (instead of using Dependency Injection),
but this can make your code harder to test, and violates the Explicit Dependencies Principle.
The following example shows how to use an instance of IDistributedCache in a simple middleware component:
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
namespace DistCacheSample
{
public class StartTimeHeader
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;
if (cacheStartTimeUTC != null)
{
startTimeUTC = Encoding.UTF8.GetString(cacheStartTimeUTC);
}
httpContext.Response.Headers.Append(
"Last-Server-Start-Time-UTC", startTimeUTC);
await _next.Invoke(httpContext);
}
}
In the code above, the cached value is read, but never written. In this sample, the value is only set when a server
starts up, and doesn't change. In a multi-server scenario, the most recent server to start will overwrite any
previous values that were set by other servers. The Get and Set methods use the byte[] type. Therefore, the
string value must be converted using Encoding.UTF8.GetString (for Get ) and Encoding.UTF8.GetBytes (for Set
).
The following code from Startup.cs shows the value being set:
public void Configure(IApplicationBuilder app,
IDistributedCache cache)
{
var startTimeUTC = DateTime.UtcNow.ToString();
byte[] encodedStartTimeUTC = Encoding.UTF8.GetBytes(startTimeUTC);
var cacheEntryOptions = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(30));
cache.Set("lastServerStartTimeUTC", encodedStartTimeUTC, cacheEntryOptions);
Since IDistributedCache is configured in the ConfigureServices method, it's available to the Configure
method as a parameter. Adding it as a parameter will allow the configured instance to be provided through DI.
To install Redis on your local machine, install the chocolatey package https://chocolatey.org/packages/redis-64/
and run redis-server from a command prompt.
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.Caching.SqlConfig.Tools"
Version="2.0.2" />
</ItemGroup>
Like all cache implementations, your app should get and set cache values using an instance of
IDistributedCache , not a SqlServerCache . The sample implements SqlServerCache in the Production
environment (so it's configured in ConfigureProductionServices ).
NOTE
The ConnectionString (and optionally, SchemaName and TableName ) should typically be stored outside of source
control (such as UserSecrets), as they may contain credentials.
Recommendations
When deciding which implementation of IDistributedCache is right for your app, choose between Redis and
SQL Server based on your existing infrastructure and environment, your performance requirements, and your
team's experience. If your team is more comfortable working with Redis, it's an excellent choice. If your team
prefers SQL Server, you can be confident in that implementation as well. Note that a traditional caching
solution stores data in-memory which allows for fast retrieval of data. You should store commonly used data in
a cache and store the entire data in a backend persistent store such as SQL Server or Azure Storage. Redis
Cache is a caching solution which gives you high throughput and low latency as compared to SQL Cache.
Additional resources
Redis Cache on Azure
SQL Database on Azure
Cache in-memory in ASP.NET Core
Detect changes with change tokens in ASP.NET Core
Response caching in ASP.NET Core
Response Caching Middleware in ASP.NET Core
Cache Tag Helper in ASP.NET Core MVC
Distributed Cache Tag Helper in ASP.NET Core
Host ASP.NET Core in a web farm
Response caching in ASP.NET Core
6/28/2018 • 8 minutes to read • Edit Online
NOTE
Response caching in Razor Pages is available in ASP.NET Core 2.1 or later.
DIRECTIVE ACTION
max-age The client won't accept a response whose age is greater than
the specified number of seconds. Examples: max-age=60 (60
seconds), max-age=2592000 (1 month)
Other cache headers that play a role in caching are shown in the following table.
HEADER FUNCTION
Vary Specifies that a cached response must not be sent unless all
of the Vary header fields match in both the cached
response's original request and the new request.
ResponseCache attribute
The ResponseCacheAttribute specifies the parameters necessary for setting appropriate headers in response
caching.
WARNING
Disable caching for content that contains information for authenticated clients. Caching should only be enabled for content
that doesn't change based on a user's identity or whether a user is signed in.
VaryByQueryKeys varies the stored response by the values of the given list of query keys. When a single value of
* is provided, the middleware varies responses by all request query string parameters. VaryByQueryKeys
requires ASP.NET Core 1.1 or later.
The Response Caching Middleware must be enabled to set the VaryByQueryKeys property; otherwise, a runtime
exception is thrown. There isn't a corresponding HTTP header for the VaryByQueryKeys property. The property is
an HTTP feature handled by the Response Caching Middleware. For the middleware to serve a cached response,
the query string and query string value must match a previous request. For example, consider the sequence of
requests and results shown in the following table.
REQUEST RESULT
The first request is returned by the server and cached in middleware. The second request is returned by
middleware because the query string matches the previous request. The third request isn't in the middleware
cache because the query string value doesn't match a previous request.
The ResponseCacheAttribute is used to configure and create (via IFilterFactory ) a ResponseCacheFilter. The
ResponseCacheFilter performs the work of updating the appropriate HTTP headers and features of the response.
The filter:
Removes any existing headers for Vary , Cache-Control , and Pragma .
Writes out the appropriate headers based on the properties set in the ResponseCacheAttribute .
Updates the response caching HTTP feature if VaryByQueryKeys is set.
Vary
This header is only written when the VaryByHeader property is set. It's set to the Vary property's value. The
following sample uses the VaryByHeader property:
You can view the response headers with your browser's network tools. The following image shows the Edge F12
output on the Network tab when the About2 action method is refreshed:
If NoStore is false and Location is None , Cache-Control and Pragma are set to no-cache .
You typically set NoStore to true on error pages. For example:
NOTE
Location 's options of Any and Client translate into Cache-Control header values of public and private ,
respectively. As noted previously, setting Location to None sets both Cache-Control and Pragma headers to
no-cache .
Below is an example showing the headers produced by setting Duration and leaving the default Location value:
[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
Cache-Control: public,max-age=60
Cache profiles
Instead of duplicating ResponseCache settings on many controller action attributes, cache profiles can be
configured as options when setting up MVC in the ConfigureServices method in Startup . Values found in a
referenced cache profile are used as the defaults by the ResponseCache attribute and are overridden by any
properties specified on the attribute.
Setting up a cache profile:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.CacheProfiles.Add("Default",
new CacheProfile()
{
Duration = 60
});
options.CacheProfiles.Add("Never",
new CacheProfile()
{
Location = ResponseCacheLocation.None,
NoStore = true
});
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
[ResponseCache(Duration = 30)]
public class HomeController : Controller
{
[ResponseCache(CacheProfileName = "Default")]
public IActionResult Index()
{
return View();
}
[ResponseCache(Duration = 30)]
public class HomeController : Controller
{
[ResponseCache(CacheProfileName = "Default")]
public IActionResult Index()
{
return View();
}
The ResponseCache attribute can be applied both to actions (methods) and controllers (classes). Method-level
attributes override the settings specified in class-level attributes.
In the above example, a class-level attribute specifies a duration of 30 seconds, while a method-level attribute
references a cache profile with a duration set to 60 seconds.
The resulting header:
Cache-Control: public,max-age=60
Additional resources
Storing Responses in Caches
Cache-Control
Cache in-memory
Work with a distributed cache
Detect changes with change tokens
Response Caching Middleware
Cache Tag Helper
Distributed Cache Tag Helper
Response Caching Middleware in ASP.NET Core
9/6/2018 • 6 minutes to read • Edit Online
Package
Reference the Microsoft.AspNetCore.App metapackage or add a package reference to the
Microsoft.AspNetCore.ResponseCaching package.
Reference the Microsoft.AspNetCore.All metapackage or add a package reference to the
Microsoft.AspNetCore.ResponseCaching package.
Add a package reference to the Microsoft.AspNetCore.ResponseCaching package.
Configuration
In Startup.ConfigureServices , add the middleware to the service collection.
services.AddResponseCaching();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
Configure the app to use the middleware with the UseResponseCaching extension method, which adds the
middleware to the request processing pipeline. The sample app adds a Cache-Control header to the response
that caches cacheable responses for up to 10 seconds. The sample sends a Vary header to configure the
middleware to serve a cached response only if the Accept-Encoding header of subsequent requests matches
that of the original request. In the code example that follows, CacheControlHeaderValue and HeaderNames
require a using statement for the Microsoft.Net.Http.Headers namespace.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseResponseCaching();
await next();
});
app.UseMvc();
}
Response Caching Middleware only caches server responses that result in a 200 (OK) status code. Any other
responses, including error pages, are ignored by the middleware.
WARNING
Responses containing content for authenticated clients must be marked as not cacheable to prevent the middleware from
storing and serving those responses. See Conditions for caching for details on how the middleware determines if a
response is cacheable.
Options
The middleware offers three options for controlling response caching.
OPTION DESCRIPTION
MaximumBodySize The largest cacheable size for the response body in bytes.
The default value is 64 * 1024 * 1024 (64 MB).
SizeLimit The size limit for the response cache middleware in bytes.
The default value is 100 * 1024 * 1024 (100 MB).
The following example configures the middleware to:
Cache responses smaller than or equal to 1,024 bytes.
Store the responses by case-sensitive paths (for example, /page1 and /Page1 are stored separately).
services.AddResponseCaching(options =>
{
options.UseCaseSensitivePaths = true;
options.MaximumBodySize = 1024;
});
VaryByQueryKeys
When using MVC/Web API controllers or Razor Pages page models, the ResponseCache attribute specifies the
parameters necessary for setting the appropriate headers for response caching. The only parameter of the
ResponseCache attribute that strictly requires the middleware is VaryByQueryKeys , which doesn't correspond to
an actual HTTP header. For more information, see ResponseCache Attribute.
When not using the ResponseCache attribute, response caching can be varied with the VaryByQueryKeys feature.
Use the ResponseCachingFeature directly from the IFeatureCollection of the HttpContext :
Using a single value equal to * in VaryByQueryKeys varies the cache by all request query parameters.
HEADER DETAILS
If-None-Match The full response is served from cache if the value isn't *
and the ETag of the response doesn't match any of the
values provided. Otherwise, a 304 (Not Modified) response
is served.
Date When serving from cache, the Date header is set by the
middleware if it wasn't provided on the original response.
Troubleshooting
If caching behavior isn't as expected, confirm that responses are cacheable and capable of being served from
the cache. Examine the request's incoming headers and the response's outgoing headers. Enable logging to help
with debugging.
When testing and troubleshooting caching behavior, a browser may set request headers that affect caching in
undesirable ways. For example, a browser may set the Cache-Control header to no-cache or max-age=0 when
refreshing a page. The following tools can explicitly set request headers and are preferred for testing caching:
Fiddler
Postman
Conditions for caching
The request must result in a server response with a 200 (OK) status code.
The request method must be GET or HEAD.
Terminal middleware, such as Static File Middleware, must not process the response prior to the Response
Caching Middleware.
The Authorization header must not be present.
Cache-Control header parameters must be valid, and the response must be marked public and not marked
private .
The Pragma: no-cache header must not be present if the Cache-Control header isn't present, as the
Cache-Control header overrides the Pragma header when present.
The Set-Cookie header must not be present.
Vary header parameters must be valid and not equal to * .
The Content-Length header value (if set) must match the size of the response body.
The IHttpSendFileFeature isn't used.
The response must not be stale as specified by the Expires header and the max-age and s-maxage cache
directives.
Response buffering must be successful, and the size of the response must be smaller than the configured or
default SizeLimit .
The response must be cacheable according to the RFC 7234 specifications. For example, the no-store
directive must not exist in request or response header fields. See Section 3: Storing Responses in Caches of
RFC 7234 for details.
NOTE
The Antiforgery system for generating secure tokens to prevent Cross-Site Request Forgery (CSRF) attacks sets the
Cache-Control and Pragma headers to no-cache so that responses aren't cached. For information on how to disable
antiforgery tokens for HTML form elements, see ASP.NET Core antiforgery configuration.
Additional resources
Application Startup
Middleware
Cache in-memory
Work with a distributed cache
Detect changes with change tokens
Response caching
Cache Tag Helper
Distributed Cache Tag Helper
Response compression in ASP.NET Core
9/24/2018 • 11 minutes to read • Edit Online
By Luke Latham
View or download sample code (how to download)
Network bandwidth is a limited resource. Reducing the size of the response usually increases the responsiveness
of an app, often dramatically. One way to reduce payload sizes is to compress an app's responses.
Response compression
Usually, any response not natively compressed can benefit from response compression. Responses not natively
compressed typically include: CSS, JavaScript, HTML, XML, and JSON. You shouldn't compress natively
compressed assets, such as PNG files. If you attempt to further compress a natively compressed response, any
small additional reduction in size and transmission time will likely be overshadowed by the time it took to process
the compression. Don't compress files smaller than about 150-1000 bytes (depending on the file's content and
the efficiency of compression). The overhead of compressing small files may produce a compressed file larger
than the uncompressed file.
When a client can process compressed content, the client must inform the server of its capabilities by sending the
Accept-Encoding header with the request. When a server sends compressed content, it must include information
in the Content-Encoding header on how the compressed response is encoded. Content encoding designations
supported by the middleware are shown in the following table.
For more information, see the IANA Official Content Coding List.
The middleware allows you to add additional compression providers for custom Accept-Encoding header values.
For more information, see Custom Providers below.
The middleware is capable of reacting to quality value (qvalue, q ) weighting when sent by the client to prioritize
compression schemes. For more information, see RFC 7231: Accept-Encoding.
Compression algorithms are subject to a tradeoff between compression speed and the effectiveness of the
compression. Effectiveness in this context refers to the size of the output after compression. The smallest size is
achieved by the most optimal compression.
The headers involved in requesting, sending, caching, and receiving compressed content are described in the table
below.
HEADER ROLE
Accept-Encoding Sent from the client to the server to indicate the content
encoding schemes acceptable to the client.
Content-Encoding Sent from the server to the client to indicate the encoding of
the content in the payload.
HEADER ROLE
Explore the features of the Response Compression Middleware with the sample app. The sample illustrates:
The compression of app responses using Gzip and custom compression providers.
How to add a MIME type to the default list of MIME types for compression.
Package
To include the middleware in a project, add a reference to the Microsoft.AspNetCore.App metapackage, which
includes the Microsoft.AspNetCore.ResponseCompression package.
To include the middleware in a project, add a reference to the Microsoft.AspNetCore.All metapackage, which
includes the Microsoft.AspNetCore.ResponseCompression package.
To include the middleware in a project, add a reference to the Microsoft.AspNetCore.ResponseCompression
package.
Configuration
The following code shows how to enable the Response Compression Middleware for default MIME types and
compression providers (Brotli and Gzip):
The following code shows how to enable the Response Compression Middleware for default MIME types and the
Gzip Compression Provider:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
}
NOTE
Use a tool like Fiddler, Firebug, or Postman to set the Accept-Encoding request header and study the response headers,
size, and body.
Submit a request to the sample app without the Accept-Encoding header and observe that the response is
uncompressed. The Content-Encoding and Vary headers aren't present on the response.
Submit a request to the sample app with the Accept-Encoding: gzip header and observe that the response is
compressed. The Content-Encoding and Vary headers are present on the response.
Providers
Brotli Compression Provider
Use the BrotliCompressionProvider to compress responses with the Brotli compressed data format.
If no compression providers are explicitly added to the CompressionProviderCollection:
The Brotli Compression Provider is added by default to the array of compression providers along with the
Gzip compression provider.
Compression defaults to Brotli compression when the Brotli compressed data format is supported by the
client. If Brotli isn't supported by the client, compression defaults to Gzip when the client supports Gzip
compression.
The Brotoli Compression Provider must be added when any compression providers are explicitly added:
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<CustomCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/svg+xml" });
});
}
Set the compression level with BrotliCompressionProviderOptions . The Brotli Compression Provider defaults to
the fastest compression level (CompressionLevel.Fastest), which might not produce the most efficient
compression. If the most efficient compression is desired, configure the middleware for optimal compression.
services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}
Set the compression level with GzipCompressionProviderOptions. The Gzip Compression Provider defaults to the
fastest compression level (CompressionLevel.Fastest), which might not produce the most efficient compression. If
the most efficient compression is desired, configure the middleware for optimal compression.
services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
}
Custom providers
Create custom compression implementations with ICompressionProvider. The EncodingName represents the
content encoding that this ICompressionProvider produces. The middleware uses this information to choose the
provider based on the list specified in the Accept-Encoding header of the request.
Using the sample app, the client submits a request with the Accept-Encoding: mycustomcompression header. The
middleware uses the custom compression implementation and returns the response with a
Content-Encoding: mycustomcompression header. The client must be able to decompress the custom encoding in
order for a custom compression implementation to work.
Submit a request to the sample app with the Accept-Encoding: mycustomcompression header and observe the
response headers. The Vary and Content-Encoding headers are present on the response. The response body (not
shown) isn't compressed by the sample. There isn't a compression implementation in the
CustomCompressionProvider class of the sample. However, the sample shows where you would implement such a
compression algorithm.
MIME types
The middleware specifies a default set of MIME types for compression:
application/javascript
application/json
application/xml
text/css
text/html
text/json
text/plain
text/xml
Replace or append MIME types with the Response Compression Middleware options. Note that wildcard MIME
types, such as text/* aren't supported. The sample app adds a MIME type for image/svg+xml and compresses
and serves the ASP.NET Core banner image (banner.svg).
Troubleshooting
Use a tool like Fiddler, Firebug, or Postman, which allow you to set the Accept-Encoding request header and study
the response headers, size, and body. By default, Response Compression Middleware compresses responses that
meet the following conditions:
The Accept-Encoding header is present with a value of br , gzip , * , or custom encoding that matches a
custom compression provider that you've established. The value must not be identity or have a quality value
(qvalue, q ) setting of 0 (zero).
The MIME type ( Content-Type ) must be set and must match a MIME type configured on the
ResponseCompressionOptions.
The request must not include the Content-Range header.
The request must use insecure protocol (http), unless secure protocol (https) is configured in the Response
Compression Middleware options. Note the danger described above when enabling secure content
compression.
The Accept-Encoding header is present with a value of gzip , * , or custom encoding that matches a custom
compression provider that you've established. The value must not be identity or have a quality value (qvalue,
q ) setting of 0 (zero).
The MIME type ( Content-Type ) must be set and must match a MIME type configured on the
ResponseCompressionOptions.
The request must not include the Content-Range header.
The request must use insecure protocol (http), unless secure protocol (https) is configured in the Response
Compression Middleware options. Note the danger described above when enabling secure content
compression.
Additional resources
Application startup in ASP.NET Core
ASP.NET Core Middleware
Mozilla Developer Network: Accept-Encoding
RFC 7231 Section 3.1.2.1: Content Codings
RFC 7230 Section 4.2.3: Gzip Coding
GZIP file format specification version 4.3
Migration to ASP.NET Core
7/10/2018 • 2 minutes to read • Edit Online
By Rick Anderson
See What's new in ASP.NET Core 2.1 for an overview of the new features in ASP.NET Core 2.1.
This article:
Covers the basics of migrating an ASP.NET Core 2.0 app to 2.1.
Provides an overview of the changes to the ASP.NET Core web application templates.
A quick way to get an overview of the changes in 2.1 is to:
Create an ASP.NET Core 2.0 web app named WebApp1.
Commit the WebApp1 in a source control system.
Delete WebApp1 and create an ASP.NET Core 2.1 web app named WebApp1 in the same place.
Review the changes in the 2.1 version.
This article provides an overview on migration to ASP.NET Core 2.1. It doesn't contain a complete list of all
changes needed to migrate to version 2.1. Some projects might require more steps depending on the options
selected when the project was created and modifications made to the project.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<UserSecretsId>aspnet-{Project Name}-{GUID}</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.4"
PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.2" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.4" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>aspnet-{Project Name}-{GUID}</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1"
PrivateAssets="All" />
</ItemGroup>
</Project>
Example:
MyApp has a package reference to Microsoft.AspNetCore.App .
MyApp.Tests has a project reference to MyApp.csproj .
Add a package reference for Microsoft.AspNetCore.App to MyApp.Tests . For more information, see
Integration testing is hard to set up and may break on shared framework servicing.
2.0 2.1
microsoft/aspnetcore:2.0 microsoft/dotnet:2.1-aspnetcore-runtime
microsoft/aspnetcore-build:2.0 microsoft/dotnet:2.1-sdk
Change the FROM lines in your Dockerfile to use the new image names and tags in the preceding table's 2.1
column. For more information, see Migrating from aspnetcore docker repos to dotnet.
The preceding image shows the 2.0 version with the deletions in red.
The following image shows the 2.1 code. The code in green replaced the 2.0 version:
The new Main replaces the call to BuildWebHost with CreateWebHostBuilder. IWebHostBuilder was added to
support a new integration test infrastructure.
Changes to Startup
The following code shows the changes to 2.1 template generated code. All changes are newly added code, except
that UseBrowserLink has been removed:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace WebApp1
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
// If the app uses Session or TempData based on Session:
// app.UseSession();
app.UseMvc();
}
}
}
/Account/Login /Identity/Account/Login
/Account/Logout /Identity/Account/Logout
/Account/Manage /Identity/Account/Manage
Applications that have code using Identity and replace 2.0 Identity UI with the 2.1 Identity Library need to take into
account Identity URLs have /Identity segment prepended to the URIs. One way to handle the new Identity
endpoints is to set up redirects, for example from /Account/Login to /Identity/Account/Login .
Update Identity to version 2.1
The following options are available to update Identity to 2.1.
Use the Identity UI 2.0 code with no changes. Using Identity UI 2.0 code is fully supported. This is a good
approach when significant changes have been made to the generated Identity code.
Delete your existing Identity 2.0 code and Scaffold Identity into your project. Your project will use the ASP.NET
Core Identity Razor Class Library. You can generate code and UI for any of the Identity UI code that you
modified. Apply your code changes to the newly scaffolded UI code.
Delete your existing Identity 2.0 code and Scaffold Identity into your project with the option to Override all
files.
Replace Identity 2.0 UI with the Identity 2.1 Razor Class Library
This section outlines the steps to replace the ASP.NET Core 2.0 template generated Identity code with the
ASP.NET Core Identity Razor Class Library. The following steps are for a Razor Pages project, but the approach
for an MVC project is similar.
Verify the project file is updated to use 2.1 versions
Delete the following folders and all the files in them:
Controllers
Pages/Account/
Extensions
Build the project.
Scaffold Identity into your project:
Select the projects exiting _Layout.cshtml file.
Select the + icon on the right side of the Data context class. Accept the default name.
Select Add to create a new Data context class. Creating a new data context is required for to scaffold.
You remove the new data context in the next section.
Update after scaffolding Identity
Delete the Identity scaffolder generated IdentityDbContext derived class in the Areas/Identity/Data/ folder.
Delete Areas/Identity/IdentityHostingStartup.cs
Update the _LoginPartial.cshtml file:
Move Pages/_LoginPartial.cshtml to Pages/Shared/_LoginPartial.cshtml
Add asp-area="Identity" to the form and anchor links.
Update the <form /> element to
<form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new {
area = "" })" method="post" id="logoutForm" class="navbar-right">
The following code shows the updated _LoginPartial.cshtml file:
@using Microsoft.AspNetCore.Identity
@if (SignInManager.IsSignedIn(User))
{
<form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new {
area = "" })" method="post" id="logoutForm" class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello
@UserManager.GetUserName(User)!</a>
</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
</li>
</ul>
</form>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="Identity" asp-page="/Account/Register">Register</a></li>
<li><a asp-area="Identity" asp-page="/Account/Login">Log in</a></li>
</ul>
}
services.AddDefaultIdentity<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
See GDPR support in ASP.NET Core for information on the preceding files.
Additional changes
If hosting the app on Windows with IIS, install the latest .NET Core Hosting Bundle.
SetCompatibilityVersion
Transport configuration
Migrate from ASP.NET to ASP.NET Core
9/13/2018 • 7 minutes to read • Edit Online
By Isaac Levin
This article serves as a reference guide for migrating ASP.NET apps to ASP.NET Core.
Prerequisites
.NET Core SDK 2.0 or later
Target frameworks
ASP.NET Core projects offer developers the flexibility of targeting .NET Core, .NET Framework, or both. See
Choosing between .NET Core and .NET Framework for server apps to determine which target framework is most
appropriate.
When targeting .NET Framework, projects need to reference individual NuGet packages.
Targeting .NET Core allows you to eliminate numerous explicit package references, thanks to the ASP.NET Core
metapackage. Install the Microsoft.AspNetCore.All metapackage in your project:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
When the metapackage is used, no packages referenced in the metapackage are deployed with the app. The .NET
Core Runtime Store includes these assets, and they're precompiled to improve performance. See
Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.x for more detail.
This approach couples the application and the server to which it's deployed in a way that interferes with the
implementation. In an effort to decouple, OWIN was introduced to provide a cleaner way to use multiple
frameworks together. OWIN provides a pipeline to add only the modules needed. The hosting environment takes a
Startup function to configure services and the app's request pipeline. Startup registers a set of middleware with
the application. For each request, the application calls each of the middleware components with the head pointer of
a linked list to an existing set of handlers. Each middleware component can add one or more handlers to the
request handling pipeline. This is accomplished by returning a reference to the handler that's the new head of the
list. Each handler is responsible for remembering and invoking the next handler in the list. With ASP.NET Core, the
entry point to an application is Startup , and you no longer have a dependency on Global.asax. When using OWIN
with .NET Framework, use something like the following as a pipeline:
using Owin;
using System.Web.Http;
namespace WebApi
{
// Note: By default all requests go through this OWIN pipeline. Alternatively you can turn this off by
adding an appSetting owin:AutomaticAppStartup with value “false”.
// With this turned off you can still have OWIN apps listening on specific routes by adding routes in
global.asax file using MapOwinPath or MapOwinRoute extensions on RouteTable.Routes
public class Startup
{
// Invoked once at startup to configure your application.
public void Configuration(IAppBuilder builder)
{
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute("Default", "{controller}/{customerID}", new { controller = "Customer",
customerID = RouteParameter.Optional });
config.Formatters.XmlFormatter.UseXmlSerializer = true;
config.Formatters.Remove(config.Formatters.JsonFormatter);
// config.Formatters.JsonFormatter.UseDataContractJsonSerializer = true;
builder.UseWebApi(config);
}
}
}
This configures your default routes, and defaults to XmlSerialization over Json. Add other Middleware to this
pipeline as needed (loading services, configuration settings, static files, etc.).
ASP.NET Core uses a similar approach, but doesn't rely on OWIN to handle the entry. Instead, that's done through
the Program.cs Main method (similar to console applications) and Startup is loaded through there.
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace WebApplication2
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
Startup must include a Configure method. In Configure , add the necessary middleware to the pipeline. In the
following example (from the default web site template), several extension methods are used to configure the
pipeline with support for:
BrowserLink
Error pages
Static files
ASP.NET Core MVC
Identity
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
The host and application have been decoupled, which provides the flexibility of moving to a different platform in
the future.
NOTE
For a more in-depth reference to ASP.NET Core Startup and Middleware, see Startup in ASP.NET Core
Store configurations
ASP.NET supports storing settings. These setting are used, for example, to support the environment to which the
applications were deployed. A common practice was to store all custom key-value pairs in the <appSettings>
section of the Web.config file:
<appSettings>
<add key="UserName" value="User" />
<add key="Password" value="Password" />
</appSettings>
ASP.NET Core can store configuration data for the application in any file and load them as part of middleware
bootstrapping. The default file used in the project templates is appsettings.json:
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
// Here is where you can supply custom configuration settings, Since it is is JSON, everything is
represented as key: value pairs
// Name of section is your choice
"AppConfiguration": {
"UserName": "UserName",
"Password": "Password"
}
}
Loading this file into an instance of IConfiguration inside your application is done in Startup.cs:
There are extensions to this approach to make the process more robust, such as using Dependency Injection (DI)
to load a service with these values. The DI approach provides a strongly-typed set of configuration objects.
NOTE
For a more in-depth reference to ASP.NET Core configuration, see Configuration in ASP.NET Core.
Create an instance of your UnityContainer , register your service, and set the dependency resolver of
HttpConfiguration to the new instance of UnityResolver for your container:
public static void Register(HttpConfiguration config)
{
var container = new UnityContainer();
container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
config.DependencyResolver = new UnityResolver(container);
Because Dependency Injection is part of ASP.NET Core, you can add your service in the ConfigureServices
method of Startup.cs:
NOTE
For more information on dependency injection, see Dependency injection.
For example, an image asset in the wwwroot/images folder is accessible to the browser at a location such as
http://<app>/images/<imageFileName> .
NOTE
For a more in-depth reference to serving static files in ASP.NET Core, see Static files.
Additional resources
Porting Libraries to .NET Core
Migrate from ASP.NET MVC to ASP.NET Core MVC
6/21/2018 • 7 minutes to read • Edit Online
NOTE
The version numbers in the samples might not be current. You may need to update your projects accordingly.
namespace WebApp1
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit
https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request
pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
The UseStaticFiles extension method adds the static file handler. As mentioned previously, the ASP.NET runtime
is modular, and you must explicitly opt in to serve static files. The UseMvc extension method adds routing. For
more information, see Application Startup and Routing.
<h1>Hello world!</h1>
http://localhost:4492/home/contact
Note the lack of styling and menu items. We'll fix that in the next section.
Static content
In previous versions of ASP.NET MVC, static content was hosted from the root of the web project and was
intermixed with server-side files. In ASP.NET Core, static content is hosted in the wwwroot folder. You'll want to
copy the static content from your old ASP.NET MVC app to the wwwroot folder in your ASP.NET Core project. In
this sample conversion:
Copy the favicon.ico file from the old MVC project to the wwwroot folder in the ASP.NET Core project.
The old ASP.NET MVC project uses Bootstrap for its styling and stores the Bootstrap files in the Content and
Scripts folders. The template, which generated the old ASP.NET MVC project, references Bootstrap in the layout
file (Views/Shared/_Layout.cshtml). You could copy the bootstrap.js and bootstrap.css files from the ASP.NET MVC
project to the wwwroot folder in the new project. Instead, we'll add support for Bootstrap (and other client-side
libraries) using CDNs in the next section.
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
@RenderSection("scripts", required: false)
</body>
</html>
View the site in the browser. It should now load correctly, with the expected styles in place.
Optional: You might want to try using the new layout file. For this project you can copy the layout file from the
FullAspNetCore project. The new layout file uses Tag Helpers and has other improvements.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace WebApp1
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?
LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
ASP.NET Core converts unhandled exceptions in a web app into HTTP 500 error responses. Normally, error
details aren't included in these responses to prevent disclosure of potentially sensitive information about the
server. See Using the Developer Exception Page in Handle errors for more information.
Additional resources
Client-side development
Tag Helpers
Migrate from ASP.NET Web API to ASP.NET Core
7/6/2018 • 8 minutes to read • Edit Online
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Routing;
namespace ProductsApp
{
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
}
WebApiConfig is defined in App_Start, and has just one static Register method:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
namespace ProductsApp
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
This class configures attribute routing, although it's not actually being used in the project. It also configures the
routing table, which is used by ASP.NET Web API. In this case, ASP.NET Web API will expect URLs to match the
format /api/{controller }/{id }, with {id } being optional.
The ProductsApp project includes just one simple controller, which inherits from ApiController and exposes two
methods:
using ProductsApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
namespace ProductsApp.Controllers
{
public class ProductsController : ApiController
{
Product[] products = new Product[]
{
new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
};
namespace ProductsApp.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
}
Now that we have a simple project from which to start, we can demonstrate how to migrate this Web API project
to ASP.NET Core MVC.
Delete the Project_Readme.html file from the new project. Your solution should now look like this:
Migrate Configuration
ASP.NET Core no longer uses Global.asax, web.config, or App_Start folders. Instead, all startup tasks are done in
Startup.cs in the root of the project (see Application Startup). In ASP.NET Core MVC, attribute-based routing is
now included by default when UseMvc() is called; and, this is the recommended approach for configuring Web API
routes (and is how the Web API starter project handles routing).
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ProductsCore
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
}
}
}
Assuming you want to use attribute routing in your project going forward, no additional configuration is needed.
Simply apply the attributes as needed to your controllers and actions, as is done in the sample ValuesController
class that's included in the Web API starter project:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace ProductsCore.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}
Note the presence of [controller ] on line 8. Attribute-based routing now supports certain tokens, such as
[controller ] and [action]. These tokens are replaced at runtime with the name of the controller or action,
respectively, to which the attribute has been applied. This serves to reduce the number of magic strings in the
project, and it ensures the routes will be kept synchronized with their corresponding controllers and actions when
automatic rename refactorings are applied.
To migrate the Products API controller, we must first copy ProductsController to the new project. Then simply
include the route attribute on the controller:
[Route("api/[controller]")]
You also need to add the [HttpGet] attribute to the two methods, since they both should be called via HTTP Get.
Include the expectation of an "id" parameter in the attribute for GetProduct() :
// /api/products
[HttpGet]
...
// /api/products/1
[HttpGet("{id}")]
At this point, routing is configured correctly; however, we can't yet test it. Additional changes must be made before
ProductsController will compile.
namespace ProductsCore.Controllers
{
[Route("api/[controller]")]
public class ProductsController : Controller
{
Product[] products = new Product[]
{
new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
};
// /api/products
[HttpGet]
public IEnumerable<Product> GetAllProducts()
{
return products;
}
// /api/products/1
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
}
You should now be able to run the migrated project and browse to /api/products; and, you should see the full list
of 3 products. Browse to /api/products/1 and you should see the first product.
Summary
Migrating a simple ASP.NET Web API project to ASP.NET Core MVC is fairly straightforward, thanks to the built-
in support for Web APIs in ASP.NET Core MVC. The main pieces every ASP.NET Web API project will need to
migrate are routes, controllers, and models, along with updates to the types used by controllers and actions.
Migrate configuration to ASP.NET Core
6/21/2018 • 2 minutes to read • Edit Online
Setup configuration
ASP.NET Core no longer uses the Global.asax and web.config files that previous versions of ASP.NET utilized. In
earlier versions of ASP.NET, application startup logic was placed in an Application_StartUp method within
Global.asax. Later, in ASP.NET MVC, a Startup.cs file was included in the root of the project; and, it was called
when the application started. ASP.NET Core has adopted this approach completely by placing all startup logic in
the Startup.cs file.
The web.config file has also been replaced in ASP.NET Core. Configuration itself can now be configured, as part of
the application startup procedure described in Startup.cs. Configuration can still utilize XML files, but typically
ASP.NET Core projects will place configuration values in a JSON -formatted file, such as appsettings.json.
ASP.NET Core's configuration system can also easily access environment variables, which can provide a more
secure and robust location for environment-specific values. This is especially true for secrets like connection strings
and API keys that shouldn't be checked into source control. See Configuration to learn more about configuration
in ASP.NET Core.
For this article, we are starting with the partially migrated ASP.NET Core project from the previous article. To
setup configuration, add the following constructor and property to the Startup.cs file located in the root of the
project:
Note that at this point, the Startup.cs file won't compile, as we still need to add the following using statement:
using Microsoft.Extensions.Configuration;
Add an appsettings.json file to the root of the project using the appropriate item template:
Migrate configuration settings from web.config
Our ASP.NET MVC project included the required database connection string in web.config, in the
<connectionStrings> element. In our ASP.NET Core project, we are going to store this information in the
appsettings.json file. Open appsettings.json, and note that it already includes the following:
{
"Data": {
"DefaultConnection": {
"ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;"
}
}
}
In the highlighted line depicted above, change the name of the database from _CHANGE_ME to the name of your
database.
Summary
ASP.NET Core places all startup logic for the application in a single file, in which the necessary services and
dependencies can be defined and configured. It replaces the web.config file with a flexible configuration feature
that can leverage a variety of file formats, such as JSON, as well as environment variables.
Migrate Authentication and Identity to ASP.NET Core
8/16/2018 • 2 minutes to read • Edit Online
By Steve Smith
In the previous article, we migrated configuration from an ASP.NET MVC project to ASP.NET Core MVC. In this
article, we migrate the registration, login, and user management features.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
At this point, there are two types referenced in the above code that we haven't yet migrated from the ASP.NET
MVC project: ApplicationDbContext and ApplicationUser . Create a new Models folder in the ASP.NET Core
project, and add two classes to it corresponding to these types. You will find the ASP.NET MVC versions of these
classes in /Models/IdentityModels.cs, but we will use one file per class in the migrated project since that's more
clear.
ApplicationUser.cs:
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace NewMvcProject.Models
{
public class ApplicationUser : IdentityUser
{
}
}
ApplicationDbContext.cs:
using Microsoft.AspNetCore.Identity.EntityFramework;
using Microsoft.Data.Entity;
namespace NewMvcProject.Models
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
The ASP.NET Core MVC Starter Web project doesn't include much customization of users, or the
ApplicationDbContext . When migrating a real app, you also need to migrate all of the custom properties and
methods of your app's user and DbContext classes, as well as any other Model classes your app utilizes. For
example, if your DbContext has a DbSet<Album> , you need to migrate the Album class.
With these files in place, the Startup.cs file can be made to compile by updating its using statements:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Our app is now ready to support authentication and Identity services. It just needs to have these features exposed
to users.
Now, add a new Razor view called _LoginPartial to the Views/Shared folder:
Update _LoginPartial.cshtml with the following code (replace all of its contents):
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@if (SignInManager.IsSignedIn(User))
{
<form asp-area="" asp-controller="Account" asp-action="Logout" method="post" id="logoutForm"
class="navbar-right">
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello
@UserManager.GetUserName(User)!</a>
</li>
<li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
</li>
</ul>
</form>
}
else
{
<ul class="nav navbar-nav navbar-right">
<li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li>
<li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li>
</ul>
}
At this point, you should be able to refresh the site in your browser.
Summary
ASP.NET Core introduces changes to the ASP.NET Identity features. In this article, you have seen how to migrate
the authentication and user management features of ASP.NET Identity to ASP.NET Core.
Migrate from ClaimsPrincipal.Current
8/16/2018 • 2 minutes to read • Edit Online
In ASP.NET 4.x projects, it was common to use ClaimsPrincipal.Current to retrieve the current authenticated user's
identity and claims. In ASP.NET Core, this property is no longer set. Code that was depending on it needs to be
updated to get the current authenticated user's identity through a different means.
The preceding sample code sets Thread.CurrentPrincipal and checks its value before and after awaiting an
asynchronous call. Thread.CurrentPrincipal is specific to the thread on which it's set, and the method is likely to
resume execution on a different thread after the await. Consequently, Thread.CurrentPrincipal is present when it's
first checked but is null after the call to await Task.Yield() .
Getting the current user's identity from the app's DI service collection is more testable, too, since test identities can
be easily injected.
By Isaac Levin
This article demonstrates migrating the database schema for ASP.NET apps using Membership authentication to
ASP.NET Core 2.0 Identity.
NOTE
This document provides the steps needed to migrate the database schema for ASP.NET Membership-based apps to the
database schema used for ASP.NET Core Identity. For more information about migrating from ASP.NET Membership-based
authentication to ASP.NET Identity, see Migrate an existing app from SQL Membership to ASP.NET Identity. For more
information about ASP.NET Core Identity, see Introduction to Identity on ASP.NET Core.
To migrate existing apps to ASP.NET Core 2.0 Identity, the data in these tables needs to be migrated to the tables
used by the new Identity schema.
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=aspnet-core-
identity;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
This command builds the database specified with the schema and any data needed for app initialization. The
following image depicts the table structure that's created with the preceding steps.
NOTE
Not all the field mappings resemble one-to-one relationships from Membership to ASP.NET Core Identity. The preceding
table takes the default Membership User schema and maps it to the ASP.NET Core Identity schema. Any other custom fields
that were used for Membership need to be mapped manually. In this mapping, there's no map for passwords, as both
password criteria and password salts don't migrate between the two. It's recommended to leave the password as null
and to ask users to reset their passwords. In ASP.NET Core Identity, LockoutEnd should be set to some date in the
future if the user is locked out. This is shown in the migration script.
Roles
IDENTITY(ASPNETROLES) MEMBERSHIP(ASPNET_ROLES)
User Roles
MEMBERSHIP(ASPNET_USERSI
IDENTITY(ASPNETUSERROLES) NROLES)
Reference the preceding mapping tables when creating a migration script for Users and Roles. The following
example assumes you have two databases on a database server. One database contains the existing ASP.NET
Membership schema and data. The other database was created using steps described earlier. Comments are
included inline for more details.
-- THIS SCRIPT NEEDS TO RUN FROM THE CONTEXT OF THE MEMBERSHIP DB
BEGIN TRANSACTION MigrateUsersAndRoles
use aspnetdb
-- INSERT USERS
INSERT INTO coreidentity.dbo.aspnetusers
(id,
username,
normalizedusername,
passwordhash,
securitystamp,
emailconfirmed,
phonenumber,
phonenumberconfirmed,
twofactorenabled,
lockoutend,
lockoutenabled,
accessfailedcount,
email,
normalizedemail)
SELECT aspnet_users.userid,
aspnet_users.username,
aspnet_users.loweredusername,
--Creates an empty password since passwords don't map between the two schemas
'',
--Security Stamp is a token used to verify the state of an account and is subject to change at any
time. It should be initialized as a new ID.
NewID(),
--EmailConfirmed is set when a new user is created and confirmed via email. Users must have this set
during migration to ensure they're able to reset passwords.
1,
aspnet_users.mobilealias,
CASE
WHEN aspnet_Users.MobileAlias is null THEN 0
ELSE 1
END,
--2-factor Auth likely wasn't setup in Membership for users, so setting as false.
0,
CASE
--Setting lockout date to time in the future (1000 years)
WHEN aspnet_membership.islockedout = 1 THEN Dateadd(year, 1000,
Sysutcdatetime())
ELSE NULL
END,
aspnet_membership.islockedout,
--AccessFailedAccount is used to track failed logins. This is stored in membership in multiple columns.
Setting to 0 arbitrarily.
0,
aspnet_membership.email,
aspnet_membership.loweredemail
FROM aspnet_users
LEFT OUTER JOIN aspnet_membership
ON aspnet_membership.applicationid =
aspnet_users.applicationid
AND aspnet_users.userid = aspnet_membership.userid
LEFT OUTER JOIN coreidentity.dbo.aspnetusers
ON aspnet_membership.userid = aspnetusers.id
WHERE aspnetusers.id IS NULL
-- INSERT ROLES
INSERT INTO coreIdentity.dbo.aspnetroles(id,name)
SELECT roleId,rolename
FROM aspnet_roles;
IF @@ERROR <> 0
BEGIN
ROLLBACK TRANSACTION MigrateUsersAndRoles
RETURN
END
After completion of this script, the ASP.NET Core Identity app created earlier is populated with Membership users.
Users need to change their passwords before logging in.
NOTE
If the Membership system had users with user names that didn't match their email address, changes are required to the app
created earlier to accommodate this. The default template expects UserName and Email to be the same. For situations in
which they're different, the login process needs to be modified to use UserName instead of Email .
In the PageModel of the Login Page, located at Pages\Account\Login.cshtml.cs, remove the [EmailAddress]
attribute from the Email property. Rename it to UserName. This requires a change wherever EmailAddress is
mentioned, in the View and PageModel. The result looks like the following:
Next steps
In this tutorial, you learned how to port users from SQL membership to ASP.NET Core 2.0 Identity. For more
information regarding ASP.NET Core Identity, see Introduction to Identity.
Migrate HTTP handlers and modules to ASP.NET
Core middleware
8/22/2018 • 15 minutes to read • Edit Online
By Matt Perdeck
This article shows how to migrate existing ASP.NET HTTP modules and handlers from system.webserver to
ASP.NET Core middleware.
Handlers are:
Classes that implement IHttpHandler
Used to handle requests with a given file name or extension, such as .report
Configured in Web.config
Modules are:
Classes that implement IHttpModule
Invoked for every request
Able to short-circuit (stop further processing of a request)
Able to add to the HTTP response, or create their own
Configured in Web.config
The order in which modules process incoming requests is determined by:
1. The application life cycle, which is a series events fired by ASP.NET: BeginRequest, AuthenticateRequest,
etc. Each module can create a handler for one or more events.
2. For the same event, the order in which they're configured in Web.config.
In addition to modules, you can add handlers for the life cycle events to your Global.asax.cs file. These handlers
run after the handlers in the configured modules.
From handlers and modules to middleware
Middleware are simpler than HTTP modules and handlers:
Modules, handlers, Global.asax.cs, Web.config (except for IIS configuration) and the application life cycle
are gone
The roles of both modules and handlers have been taken over by middleware
Middleware are configured using code rather than in Web.config
Pipeline branching lets you send requests to specific middleware, based on not only the URL but also on
request headers, query strings, etc.
Middleware are very similar to modules:
Invoked in principle for every request
Able to short-circuit a request, by not passing the request to the next middleware
Able to create their own HTTP response
Middleware and modules are processed in a different order:
Order of middleware is based on the order in which they're inserted into the request pipeline, while order
of modules is mainly based on application life cycle events
Order of middleware for responses is the reverse from that for requests, while order of modules is the
same for requests and responses
See Create a middleware pipeline with IApplicationBuilder
Note how in the image above, the authentication middleware short-circuited the request.
using System;
using System.Web;
namespace MyApp.Modules
{
public class MyModule : IHttpModule
{
public void Dispose()
{
}
As shown in the Middleware page, an ASP.NET Core middleware is a class that exposes an Invoke method
taking an HttpContext and returning a Task . Your new middleware will look like this:
// ASP.NET Core middleware
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace MyApp.Middleware
{
public class MyMiddleware
{
private readonly RequestDelegate _next;
await _next.Invoke(context);
// Clean up.
}
}
The preceding middleware template was taken from the section on writing middleware.
The MyMiddlewareExtensions helper class makes it easier to configure your middleware in your Startup class.
The UseMyMiddleware method adds your middleware class to the request pipeline. Services required by the
middleware get injected in the middleware's constructor.
Your module might terminate a request, for example if the user isn't authorized:
if (TerminateRequest())
{
context.Response.End();
return;
}
}
A middleware handles this by not calling Invoke on the next middleware in the pipeline. Keep in mind that this
doesn't fully terminate the request, because previous middlewares will still be invoked when the response makes
its way back through the pipeline.
// ASP.NET Core middleware that may terminate the request
if (!TerminateRequest())
await _next.Invoke(context);
// Clean up.
}
When you migrate your module's functionality to your new middleware, you may find that your code doesn't
compile because the HttpContext class has significantly changed in ASP.NET Core. Later on, you'll see how to
migrate to the new ASP.NET Core HttpContext.
Convert this by adding your new middleware to the request pipeline in your Startup class:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseMyMiddleware();
app.UseMyMiddlewareWithParams();
app.UseMyTerminatingMiddleware();
app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".context"),
appBranch => {
appBranch.UseHttpContextDemoMiddleware();
});
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
The exact spot in the pipeline where you insert your new middleware depends on the event that it handled as a
module ( BeginRequest , EndRequest , etc.) and its order in your list of modules in Web.config.
As previously stated, there's no application life cycle in ASP.NET Core and the order in which responses are
processed by middleware differs from the order used by modules. This could make your ordering decision more
challenging.
If ordering becomes a problem, you could split your module into multiple middleware components that can be
ordered independently.
// ASP.NET 4 handler
using System.Web;
namespace MyApp.HttpHandlers
{
public class MyHandler : IHttpHandler
{
public bool IsReusable { get { return true; } }
context.Response.ContentType = GetContentType();
context.Response.Output.Write(response);
}
// ...
In your ASP.NET Core project, you would translate this to a middleware similar to this:
// ASP.NET Core middleware migrated from a handler
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace MyApp.Middleware
{
public class MyHandlerMiddleware
{
// Must have constructor with this signature, otherwise exception at run time
public MyHandlerMiddleware(RequestDelegate next)
{
// This is an HTTP Handler, so no need to store next
}
context.Response.ContentType = GetContentType();
await context.Response.WriteAsync(response);
}
// ...
This middleware is very similar to the middleware corresponding to modules. The only real difference is that here
there's no call to _next.Invoke(context) . That makes sense, because the handler is at the end of the request
pipeline, so there will be no next middleware to invoke.
You could convert this by adding your new handler middleware to the request pipeline in your Startup class,
similar to middleware converted from modules. The problem with that approach is that it would send all requests
to your new handler middleware. However, you only want requests with a given extension to reach your
middleware. That would give you the same functionality you had with your HTTP handler.
One solution is to branch the pipeline for requests with a given extension, using the MapWhen extension method.
You do this in the same Configure method where you add the other middleware:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseMyMiddleware();
app.UseMyMiddlewareWithParams();
app.UseMyTerminatingMiddleware();
app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".context"),
appBranch => {
appBranch.UseHttpContextDemoMiddleware();
});
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
{
"MyMiddlewareOptionsSection": {
"Param1": "Param1Value",
"Param2": "Param2Value"
}
}
MyMiddlewareOptionsSection here is a section name. It doesn't have to be the same as the name of your
options class.
3. Associate the option values with the options class
The options pattern uses ASP.NET Core's dependency injection framework to associate the options type
(such as MyMiddlewareOptions ) with a MyMiddlewareOptions object that has the actual options.
Update your Startup class:
a. If you're using appsettings.json, add it to the configuration builder in the Startup constructor:
4. Inject the options into your middleware constructor. This is similar to injecting options into a controller.
await _next.Invoke(context);
The UseMiddleware extension method that adds your middleware to the IApplicationBuilder takes care
of dependency injection.
This isn't limited to IOptions objects. Any other object that your middleware requires can be injected this
way.
{
"MyMiddlewareOptionsSection2": {
"Param1": "Param1Value2",
"Param2": "Param2Value2"
},
"MyMiddlewareOptionsSection": {
"Param1": "Param1Value",
"Param2": "Param2Value"
}
}
2. Retrieve options values and pass them to middleware. The Use... extension method (which adds your
middleware to the pipeline) is a logical place to pass in the option values:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseMyMiddleware();
app.UseMyMiddlewareWithParams();
var myMiddlewareOptions =
Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
var myMiddlewareOptions2 =
Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
app.UseMyMiddlewareWithParams(myMiddlewareOptions);
app.UseMyMiddlewareWithParams(myMiddlewareOptions2);
app.UseMyTerminatingMiddleware();
app.MapWhen(
context => context.Request.Path.ToString().EndsWith(".context"),
appBranch => {
appBranch.UseHttpContextDemoMiddleware();
});
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
3. Enable middleware to take an options parameter. Provide an overload of the Use... extension method
(that takes the options parameter and passes it to UseMiddleware ). When UseMiddleware is called with
parameters, it passes the parameters to your middleware constructor when it instantiates the middleware
object.
public static class MyMiddlewareWithParamsExtensions
{
public static IApplicationBuilder UseMyMiddlewareWithParams(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddlewareWithParams>();
}
Note how this wraps the options object in an OptionsWrapper object. This implements IOptions , as
expected by the middleware constructor.
HttpContext has significantly changed in ASP.NET Core. This section shows how to translate the most commonly
used properties of System.Web.HttpContext to the new Microsoft.AspNetCore.Http.HttpContext .
HttpContext
HttpContext.Items translates to:
HttpContext.Request
HttpContext.Request.HttpMethod translates to:
// using Microsoft.AspNetCore.Http.Extensions;
var url = httpContext.Request.GetDisplayUrl();
// using Microsoft.AspNetCore.Http.Headers;
// using Microsoft.Net.Http.Headers;
// For unknown header, unknownheaderValues has zero items and unknownheaderValue is ""
IList<string> unknownheaderValues = headersDictionary["unknownheader"];
string unknownheaderValue = headersDictionary["unknownheader"].ToString();
// For known header, knownheaderValues has 1 item and knownheaderValue is the value
IList<string> knownheaderValues = headersDictionary[HeaderNames.AcceptLanguage];
string knownheaderValue = headersDictionary[HeaderNames.AcceptLanguage].ToString();
// using Microsoft.Net.Http.Headers;
if (httpContext.Request.HasFormContentType)
{
IFormCollection form;
WARNING
Read form values only if the content sub type is x-www-form-urlencoded or form-data.
string inputBody;
using (var reader = new System.IO.StreamReader(
httpContext.Request.Body, System.Text.Encoding.UTF8))
{
inputBody = reader.ReadToEnd();
}
WARNING
Use this code only in a handler type middleware, at the end of a pipeline.
You can read the raw body as shown above only once per request. Middleware trying to read the body after the first read
will read an empty body.
This doesn't apply to reading a form as shown earlier, because that's done from a buffer.
HttpContext.Response
HttpContext.Response.Status and HttpContext.Response.StatusDescription translate to:
// using Microsoft.AspNetCore.Http;
httpContext.Response.StatusCode = StatusCodes.Status200OK;
// using Microsoft.Net.Http.Headers;
var mediaType = new MediaTypeHeaderValue("application/json");
mediaType.Encoding = System.Text.Encoding.UTF8;
httpContext.Response.ContentType = mediaType.ToString();
httpContext.Response.ContentType = "text/html";
HttpContext.Response.TransmitFile
Serving up a file is discussed here.
HttpContext.Response.Headers
Sending response headers is complicated by the fact that if you set them after anything has been written to the
response body, they will not be sent.
The solution is to set a callback method that will be called right before writing to the response starts. This is best
done at the start of the Invoke method in your middleware. It's this callback method that sets your response
headers.
The following code sets a callback method called SetHeaders :
HttpContext.Response.Cookies
Cookies travel to the browser in a Set-Cookie response header. As a result, sending cookies requires the same
callback as used for sending response headers:
responseCookies.Append("cookie1name", "cookie1value");
responseCookies.Append("cookie2name", "cookie2value",
new CookieOptions { Expires = System.DateTime.Now.AddDays(5), HttpOnly = true });
Additional resources
HTTP Handlers and HTTP Modules Overview
Configuration
Application Startup
Middleware
Migrate from ASP.NET Core 1.x to 2.0
6/21/2018 • 7 minutes to read • Edit Online
By Scott Addie
In this article, we walk you through updating an existing ASP.NET Core 1.x project to ASP.NET Core 2.0.
Migrating your application to ASP.NET Core 2.0 enables you to take advantage of many new features and
performance improvements.
Existing ASP.NET Core 1.x applications are based off of version-specific project templates. As the ASP.NET Core
framework evolves, so do the project templates and the starter code contained within them. In addition to
updating the ASP.NET Core framework, you need to update the code for your application.
Prerequisites
See Get Started with ASP.NET Core.
<TargetFramework>netcoreapp2.0</TargetFramework>
Projects targeting .NET Framework should use the TFM of a version greater than or equal to .NET Framework
4.6.1. Search for the <TargetFramework> node in the .csproj file, and replace its inner text with net461 :
<TargetFramework>net461</TargetFramework>
NOTE
.NET Core 2.0 offers a much larger surface area than .NET Core 1.x. If you're targeting .NET Framework solely because of
missing APIs in .NET Core 1.x, targeting .NET Core 2.0 is likely to work.
{
"sdk": {
"version": "2.0.0"
}
}
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.4" />
</ItemGroup>
All the features of ASP.NET Core 2.0 and Entity Framework Core 2.0 are included in the metapackage.
ASP.NET Core 2.0 projects targeting .NET Framework should continue to reference individual NuGet packages.
Update the Version attribute of each <PackageReference /> node to 2.0.0.
For example, here's the list of <PackageReference /> nodes used in a typical ASP.NET Core 2.0 project targeting
.NET Framework:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0"
PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0"
PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
Update Main method in Program.cs
In 1.x projects, the Main method of Program.cs looked like this:
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace AspNetCoreDotNetCore1App
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
host.Run();
}
}
}
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace AspNetCoreDotNetCore2App
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
The adoption of this new 2.0 pattern is highly recommended and is required for product features like Entity
Framework (EF ) Core Migrations to work. For example, running Update-Database from the Package Manager
Console window or dotnet ef database update from the command line (on projects converted to ASP.NET Core
2.0) generates the following error:
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
The preceding example loads the Configuration member with configuration settings from appsettings.json as well
as any appsettings.<EnvironmentName>.json file matching the IHostingEnvironment.EnvironmentName property.
The location of these files is at the same path as Startup.cs.
In 2.0 projects, the boilerplate configuration code inherent to 1.x projects runs behind-the-scenes. For example,
environment variables and app settings are loaded at startup. The equivalent Startup.cs code is reduced to
IConfiguration initialization with the injected instance:
To remove the default providers added by WebHostBuilder.CreateDefaultBuilder , invoke the Clear method on the
IConfigurationBuilder.Sources property inside of ConfigureAppConfiguration . To add providers back, utilize the
ConfigureAppConfiguration method in Program.cs:
The configuration used by the CreateDefaultBuilder method in the preceding code snippet can be seen here.
For more information, see Configuration in ASP.NET Core.
Move database initialization code
In 1.x projects using EF Core 1.x, a command such as dotnet ef migrations add does the following:
1. Instantiates a Startup instance
2. Invokes the ConfigureServices method to register all services with dependency injection (including DbContext
types)
3. Performs its requisite tasks
In 2.0 projects using EF Core 2.0, Program.BuildWebHost is invoked to obtain the application services. Unlike 1.x,
this has the additional side effect of invoking Startup.Configure . If your 1.x app invoked database initialization
code in its Configure method, unexpected problems can occur. For example, if the database doesn't yet exist, the
seeding code runs before the EF Core Migrations command execution. This problem causes a
dotnet ef migrations list command to fail if the database doesn't yet exist.
Consider the following 1.x seed initialization code in the Configure method of Startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
SeedData.Initialize(app.ApplicationServices);
In 2.0 projects, move the SeedData.Initialize call to the Main method of Program.cs:
try
{
// Requires using RazorPagesMovie.Models;
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
As of 2.0, it's bad practice to do anything in BuildWebHost except build and configure the web host. Anything that's
about running the application should be handled outside of BuildWebHost — typically in the Main method of
Program.cs.
2. If targeting .NET Core, remove the UseApplicationInsights extension method invocation from Program.cs:
host.Run();
}
3. Remove the Application Insights client-side API call from _Layout.cshtml. It comprises the following two
lines of code:
If you are using the Application Insights SDK directly, continue to do so. The 2.0 metapackage includes the latest
version of Application Insights, so a package downgrade error appears if you're referencing an older version.
Additional resources
Breaking Changes in ASP.NET Core 2.0
Migrate authentication and Identity to ASP.NET Core
2.0
8/16/2018 • 8 minutes to read • Edit Online
In 2.0 projects, authentication is configured via services. Each authentication scheme is registered in the
ConfigureServices method of Startup.cs. The UseIdentity method is replaced with UseAuthentication .
The following 2.0 example configures Facebook authentication with Identity in Startup.cs:
app.UseAuthentication();
Invoke the AddIdentity method in the ConfigureServices method to add the cookie authentication
services.
Optionally, invoke the ConfigureApplicationCookie or ConfigureExternalCookie method in the
ConfigureServices method to tweak the Identity cookie settings.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
app.UseAuthentication();
app.UseAuthentication();
This code snippet doesn't use Identity, so the default scheme should be set by passing
JwtBearerDefaults.AuthenticationScheme to the AddAuthentication method.
app.UseAuthentication();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["auth:oidc:authority"];
options.ClientId = Configuration["auth:oidc:clientid"];
});
Facebook authentication
Make the following changes in Startup.cs:
Replace the UseFacebookAuthentication method call in the Configure method with UseAuthentication :
app.UseAuthentication();
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
});
Google authentication
Make the following changes in Startup.cs:
Replace the UseGoogleAuthentication method call in the Configure method with UseAuthentication :
app.UseAuthentication();
app.UseAuthentication();
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["auth:microsoft:clientid"];
options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
});
Twitter authentication
Make the following changes in Startup.cs:
Replace the UseTwitterAuthentication method call in the Configure method with UseAuthentication :
app.UseAuthentication();
services.AddAuthentication()
.AddTwitter(options =>
{
options.ConsumerKey = Configuration["auth:twitter:consumerkey"];
options.ConsumerSecret = Configuration["auth:twitter:consumersecret"];
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
An exception to this rule is the AddIdentity method. This method adds cookies for you and sets the default
authenticate and challenge schemes to the application cookie IdentityConstants.ApplicationScheme . Additionally, it
sets the default sign-in scheme to the external cookie IdentityConstants.ExternalScheme .
In 2.0 projects, import the Microsoft.AspNetCore.Authentication namespace, and delete the Authentication
property references:
services.AddAuthentication(IISDefaults.AuthenticationScheme);
Failure to set the default scheme accordingly prevents the authorize request to challenge from working.
IdentityCookieOptions instances
A side effect of the 2.0 changes is the switch to using named options instead of cookie options instances. The
ability to customize the Identity cookie scheme names is removed.
For example, 1.x projects use constructor injection to pass an IdentityCookieOptions parameter into
AccountController.cs. The external cookie authentication scheme is accessed from the provided instance:
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityCookieOptions> identityCookieOptions,
IEmailSender emailSender,
ISmsSender smsSender,
ILoggerFactory loggerFactory)
{
_userManager = userManager;
_signInManager = signInManager;
_externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
_emailSender = emailSender;
_smsSender = smsSender;
_logger = loggerFactory.CreateLogger<AccountController>();
}
The aforementioned constructor injection becomes unnecessary in 2.0 projects, and the _externalCookieScheme
field can be deleted:
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
ISmsSender smsSender,
ILoggerFactory loggerFactory)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_smsSender = smsSender;
_logger = loggerFactory.CreateLogger<AccountController>();
}
/// <summary>
/// Navigation property for the claims this user possesses.
/// </summary>
public virtual ICollection<IdentityUserClaim<int>> Claims { get; } = new List<IdentityUserClaim<int>>();
/// <summary>
/// Navigation property for this users login accounts.
/// </summary>
public virtual ICollection<IdentityUserLogin<int>> Logins { get; } = new List<IdentityUserLogin<int>>();
To prevent duplicate foreign keys when running EF Core Migrations, add the following to your IdentityDbContext
class' OnModelCreating method (after the base.OnModelCreating(); call):
builder.Entity<ApplicationUser>()
.HasMany(e => e.Claims)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<ApplicationUser>()
.HasMany(e => e.Logins)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<ApplicationUser>()
.HasMany(e => e.Roles)
.WithOne()
.HasForeignKey(e => e.UserId)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
Replace GetExternalAuthenticationSchemes
The synchronous method GetExternalAuthenticationSchemes was removed in favor of an asynchronous version.
1.x projects have the following code in ManageController.cs:
In Login.cshtml, the AuthenticationScheme property accessed in the foreach loop changes to Name :
using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
namespace AspNetCoreDotNetCore1App.Models.ManageViewModels
{
public class ManageLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
In 2.0 projects, the return type changes to IList<AuthenticationScheme> . This new return type requires replacing
the Microsoft.AspNetCore.Http.Authentication import with a Microsoft.AspNetCore.Authentication import.
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
namespace AspNetCoreDotNetCore2App.Models.ManageViewModels
{
public class ManageLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
Additional resources
For additional details and discussion, see the Discussion for Auth 2.0 issue on GitHub.