Blazor For ASP - Net Web Forms Developers
Blazor For ASP - Net Web Forms Developers
EDITION v1.0
Refer changelog for the book updates and community contributions.
PUBLISHED BY
Microsoft Developer Division, .NET, and Visual Studio product teams
A division of Microsoft Corporation
One Microsoft Way
Redmond, Washington 98052-6399
Copyright © 2021 by Microsoft Corporation
All rights reserved. No part of the contents of this book may be reproduced or transmitted in any form or by
any means without the written permission of the publisher.
This book is provided "as-is" and expresses the author's views and opinions. The views, opinions, and
information expressed in this book, including URL and other Internet website references, may change without
notice.
Some examples depicted herein are provided for illustration only and are fictitious. No real association or
connection is intended or should be inferred.
Microsoft and the trademarks listed at https://www.microsoft.com on the "Trademarks" webpage are trademarks
of the Microsoft group of companies.
Mac and macOS are trademarks of Apple Inc.
All other marks and logos are property of their respective owners.
Authors:
Steve "ardalis" Smith , Software Architect and Trainer, Ardalis Services LLC
Introduction
.NET has long supported web app development through ASP.NET, a comprehensive set of frameworks and tools
for building any kind of web app. ASP.NET has its own lineage of web frameworks and technologies starting all
the way back with classic Active Server Pages (ASP). Frameworks like ASP.NET Web Forms, ASP.NET MVC,
ASP.NET Web Pages, and more recently ASP.NET Core, provide a productive and powerful way to build server-
rendered web apps, where UI content is dynamically generated on the server in response to HTTP requests. Each
ASP.NET framework caters to a different audience and app building philosophy. ASP.NET Web Forms shipped
with the original release of the .NET Framework and enabled web development using many of the patterns
familiar to desktop developers, like reusable UI controls with simple event handling. However, none of the
ASP.NET offerings provide a way to run code that executed in the user's browser. To do that requires writing
JavaScript and using any of the many JavaScript frameworks and tools that have phased in and out of
popularity over the years: jQuery, Knockout, Angular, React, and so on.
Blazor is a new web framework that changes what is possible when building web apps with .NET. Blazor is a
client-side web UI framework based on C# instead of JavaScript. With Blazor you can write your client-side logic
and UI components in C#, compile them into normal .NET assemblies, and then run them directly in the browser
using a new open web standard called WebAssembly. Or alternatively, Blazor can run your .NET UI components
on the server and handle all UI interactions fluidly over a real-time connection with the browser. When paired
with .NET running on the server, Blazor enables full-stack web development with .NET. While Blazor shares many
commonalities with ASP.NET Web Forms, like having a reusable component model and a simple way to handle
user events, it also builds on the foundations of .NET to provide a modern and high-performance web
development experience.
This book introduces ASP.NET Web Forms developers to Blazor in a way that is familiar and convenient. It
introduces Blazor concepts in parallel with analogous concepts in ASP.NET Web Forms while also explaining new
concepts that may be less familiar. It covers a broad range of topics and concerns including component
authoring, routing, layout, configuration, and security. And while the content of this book is primarily for
enabling new development, it also covers guidelines and strategies for migrating existing ASP.NET Web Forms
to Blazor for when you want to modernize an existing app.
NE XT
An introduction to Blazor for ASP.NET Web Forms
developers
3/6/2021 • 8 minutes to read • Edit Online
The ASP.NET Web Forms framework has been a staple of .NET web development since the .NET Framework first
shipped in 2002. Back when the Web was still largely in its infancy, ASP.NET Web Forms made building web
apps simple and productive by adopting many of the patterns that were used for desktop development. In
ASP.NET Web Forms, web pages can be quickly composed from reusable UI controls. User interactions are
handled naturally as events. There's a rich ecosystem of Web Forms UI controls provided by Microsoft and
control vendors. The controls ease the efforts of connecting to data sources and displaying rich data
visualizations. For the visually inclined, the Web Forms designer provides a simple drag-and-drop interface for
managing controls.
Over the years, Microsoft has introduced new ASP.NET-based web frameworks to address web development
trends. Some such web frameworks include ASP.NET MVC, ASP.NET Web Pages, and more recently ASP.NET Core.
With each new framework, some have predicted the imminent decline of ASP.NET Web Forms and criticized it as
an outdated, outmoded web framework. Despite these predictions, many .NET web developers continue to find
ASP.NET Web Forms a simple, stable, and productive way to get their work done.
At the time of writing, almost half a million web developers use ASP.NET Web Forms every month. The ASP.NET
Web Forms framework is stable to the point that docs, samples, books, and blog posts from a decade ago
remain useful and relevant. For many .NET web developers, "ASP.NET" is still synonymous with "ASP.NET Web
Forms" as it was when .NET was first conceived. Arguments on the pros and cons of ASP.NET Web Forms
compared to the other new .NET web frameworks may rage on. ASP.NET Web Forms remains a popular
framework for creating web apps.
Even so, innovations in software development aren't slowing. All software developers need to stay abreast of
new technologies and trends. Two trends in particular are worth considering:
1. The shift to open-source and cross-platform
2. The shift of app logic to the client
PR EVIO U S NE XT
Architecture comparison of ASP.NET Web Forms
and Blazor
3/6/2021 • 3 minutes to read • Edit Online
While ASP.NET Web Forms and Blazor have many similar concepts, there are differences in how they work. This
chapter examines the inner workings and architectures of ASP.NET Web Forms and Blazor.
Blazor
Blazor is a client-side web UI framework similar in nature to JavaScript front-end frameworks like Angular or
React. Blazor handles user interactions and renders the necessary UI updates. Blazor isn't based on a request-
reply model. User interactions are handled as events that aren't in the context of any particular HTTP request.
Blazor apps consist of one or more root components that are rendered on an HTML page.
How the user specifies where components should render and how the components are then wired up for user
interactions is hosting model specific.
Blazor components are .NET classes that represent a reusable piece of UI. Each component maintains its own
state and specifies its own rendering logic, which can include rendering other components. Components specify
event handlers for specific user interactions to update the component's state.
After a component handles an event, Blazor renders the component and keeps track of what changed in the
rendered output. Components don't render directly to the Document Object Model (DOM). They instead render
to an in-memory representation of the DOM called a RenderTree so that Blazor can track the changes. Blazor
compares the newly rendered output with the previous output to calculate a UI diff that it then applies efficiently
to the DOM.
Components can also manually indicate that they should be rendered if their state changes outside of a normal
UI event. Blazor uses a SynchronizationContext to enforce a single logical thread of execution. A component's
lifecycle methods and any event callbacks that are raised by Blazor are executed on this SynchronizationContext .
PR EVIO U S NE XT
Blazor app hosting models
3/6/2021 • 5 minutes to read • Edit Online
Blazor WebAssembly apps run purely client-side. Such apps can be deployed to static site hosting solutions like
GitHub Pages or Azure Static Website Hosting. .NET isn't required on the server at all. Deep linking to parts of
the app typically requires a routing solution on the server. The routing solution redirects requests to the root of
the app. For example, this redirection can be handled using URL rewrite rules in IIS.
To get all the benefits of Blazor and full-stack .NET web development, host your Blazor WebAssembly app with
ASP.NET Core. By using .NET on both the client and server, you can easily share code and build your app using
one consistent set of languages, frameworks, and tools. Blazor provides convenient templates for setting up a
solution that contains both a Blazor WebAssembly app and an ASP.NET Core host project. When the solution is
built, the built static files from the Blazor app are hosted by the ASP.NET Core app with fallback routing already
setup.
The Blazor Server hosting model may sound familiar if you've used ASP.NET AJAX and the UpdatePanel control.
The UpdatePanel control handles applying partial page updates in response to trigger events on the page. When
triggered, the UpdatePanel requests a partial update and then applies it without needing to refresh the page.
The state of the UI is managed using ViewState . Blazor Server apps are slightly different in that the app requires
an active connection with the client. Additionally, all UI state is maintained on the server. Aside from those
differences, the two models are conceptually similar.
PR EVIO U S NE XT
Project structure for Blazor apps
3/6/2021 • 8 minutes to read • Edit Online
Despite their significant project structure differences, ASP.NET Web Forms and Blazor share many similar
concepts. Here, we'll look at the structure of a Blazor project and compare it to an ASP.NET Web Forms project.
To create your first Blazor app, follow the instructions in the Blazor getting started steps. You can follow the
instructions to create either a Blazor Server app or a Blazor WebAssembly app hosted in ASP.NET Core. Except
for the hosting model-specific logic, most of the code in both projects is the same.
Project file
Blazor Server apps are .NET projects. The project file for the Blazor Server app is about as simple as it can get:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
The project file for a Blazor WebAssembly app looks slightly more involved (exact version numbers may vary):
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.0"
PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
</ItemGroup>
</Project>
Although they're supported, individual assembly references are less common in .NET projects. Most project
dependencies are handled as NuGet package references. You only need to reference top-level package
dependencies in .NET projects. Transitive dependencies are included automatically. Instead of using the
packages.config file commonly found in ASP.NET Web Forms projects to reference packages, package references
are added to the project file using the <PackageReference> element.
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
</ItemGroup>
Entry point
The Blazor Server app's entry point is defined in the Program.cs file, as you would see in a Console app. When
the app executes, it creates and runs a web host instance using defaults specific to web apps. The web host
manages the Blazor Server app's lifecycle and sets up host-level services. Examples of such services are
configuration, logging, dependency injection, and the HTTP server. This code is mostly boilerplate and is often
left unchanged.
Blazor WebAssembly apps also define an entry point in Program.cs. The code looks slightly different. The code is
similar in that it's setting up the app host to provide the same host-level services to the app. The WebAssembly
app host doesn't, however, set up an HTTP server because it executes directly in the browser.
Blazor apps have a Startup class instead of a Global.asax file to define the startup logic for the app. The
Startup class is used to configure the app and any app-specific services. In the Blazor Server app, the Startup
class is used to set up the endpoint for the real-time connection used by Blazor between the client browsers and
the server. In the Blazor WebAssembly app, the Startup class defines the root components for the app and
where they should be rendered. We'll take a deeper look at the Startup class in the App startup section.
Static files
Unlike ASP.NET Web Forms projects, not all files in a Blazor project can be requested as static files. Only the files
in the wwwroot folder are web-addressable. This folder is referred to the app's "web root". Anything outside of
the app's web root isn't web-addressable. This setup provides an additional level of security that prevents
accidental exposing of project files over the web.
Configuration
Configuration in ASP.NET Web Forms apps is typically handled using one or more web.config files. Blazor apps
don't typically have web.config files. If they do, the file is only used to configure IIS-specific settings when hosted
on IIS. Instead, Blazor Server apps use the ASP.NET Core configuration abstractions (Blazor WebAssembly apps
don't currently support the same configuration abstractions, but that may be a feature added in the future). For
example, the default Blazor Server app stores some settings in appsettings.json.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
We'll learn more about configuration in ASP.NET Core projects in the Configuration section.
Razor components
Most files in Blazor projects are .razor files. Razor is a templating language based on HTML and C# that is used
to dynamically generate web UI. The .razor files define components that make up the UI of the app. For the most
part, the components are identical for both the Blazor Server and Blazor WebAssembly apps. Components in
Blazor are analogous to user controls in ASP.NET Web Forms.
Each Razor component file is compiled into a .NET class when the project is built. The generated class captures
the component's state, rendering logic, lifecycle methods, event handlers, and other logic. We'll look at authoring
components in the Building reusable UI components with Blazor section.
The _Imports.razor files aren't Razor component files. Instead, they define a set of Razor directives to import into
other .razor files within the same folder and in its subfolders. For example, a _Imports.razor file is a conventional
way to add using directives for commonly used namespaces:
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BlazorApp1
@using BlazorApp1.Shared
Pages
Where are the pages in the Blazor apps? Blazor doesn't define a separate file extension for addressable pages,
like the .aspx files in ASP.NET Web Forms apps. Instead, pages are defined by assigning routes to components. A
route is typically assigned using the @page Razor directive. For example, the Counter component authored in
the Pages/Counter.razor file defines the following route:
@page "/counter"
Routing in Blazor is handled client-side, not on the server. As the user navigates in the browser, Blazor intercepts
the navigation and then renders the component with the matching route.
The component routes aren't currently inferred by the component's file location like they are with .aspx pages.
This feature may be added in the future. Each route must be specified explicitly on the component. Storing
routable components in a Pages folder has no special meaning and is purely a convention.
We'll look in greater detail at routing in Blazor in the Pages, routing, and layouts section.
Layout
In ASP.NET Web Forms apps, a common page layout is handled using master pages (Site.Master). In Blazor apps,
the page layout is handled using layout components (Shared/MainLayout.razor). Layout components will be
discussed in more detail in Page, routing, and layouts section.
Bootstrap Blazor
To bootstrap Blazor, the app must:
Specify where on the page the root component (App.Razor) should be rendered.
Add the corresponding Blazor framework script.
In the Blazor Server app, the root component's host page is defined in the _Host.cshtml file. This file defines a
Razor Page, not a component. Razor Pages use Razor syntax to define a server-addressable page, very much like
an .aspx page. The Html.RenderComponentAsync<TComponent>(RenderMode) method is used to define where a root-
level component should be rendered. The RenderMode option indicates the manner in which the component
should be rendered. The following table outlines the supported RenderMode options.
O P T IO N DESC RIP T IO N
The script reference to _framework/blazor.server.js establishes the real-time connection with the server and then
deals with all user interactions and UI updates.
@page "/"
@namespace BlazorApp1.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlazorApp1</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
In the Blazor WebAssembly app, the host page is a simple static HTML file under wwwroot/index.html. The
<div> element with id named app is used to indicate where the root component should be rendered.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-
scalable=no" />
<title>BlazorApp2</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="blazor-web.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss"> </a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
The root component to render is specified in the app's Program.Main method with the flexibility to register
services through dependency injection. For more information, see ASP.NET Core Blazor dependency injection.
....
....
}
}
Build output
When a Blazor project is built, all Razor component and code files are compiled into a single assembly. Unlike
ASP.NET Web Forms projects, Blazor doesn't support runtime compilation of the UI logic.
PR EVIO U S NE XT
App startup
3/6/2021 • 5 minutes to read • Edit Online
Applications that are written for ASP.NET typically have a global.asax.cs file that defines the Application_Start
event that controls which services are configured and made available for both HTML rendering and .NET
processing. This chapter looks at how things are slightly different with ASP.NET Core and Blazor Server.
Each of these individual files resides in the App_Start folder and run only once at the start of our application.
RouteConfig in the default project template adds the FriendlyUrlSettings for web forms to allow application
URLs to omit the .ASPX file extension. The default template also contains a directive that provides permanent
HTTP redirect status codes (HTTP 301) for the .ASPX pages to the friendly URL with the file name that omits the
extension.
With ASP.NET Core and Blazor, these methods are either simplified and consolidated into the Startup class or
they are eliminated in favor of common web technologies.
// 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.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see
https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
Like the rest of ASP.NET Core, the Startup class is created with dependency injection principles. The
IConfiguration is provided to the constructor and stashed in a public property for later access during
configuration.
The ConfigureServices method introduced in ASP.NET Core allows for the various ASP.NET Core framework
services to be configured for the framework's built-in dependency injection container. The various
services.Add* methods add services that enable features such as authentication, razor pages, MVC controller
routing, SignalR, and Blazor Server interactions among many others. This method was not needed in web forms,
as the parsing and handling of the ASPX, ASCX, ASHX, and ASMX files were defined by referencing ASP.NET in
the web.config configuration file. More information about dependency injection in ASP.NET Core is available in
the online documentation.
The Configure method introduces the concept of the HTTP pipeline to ASP.NET Core. In this method, we declare
from top to bottom the Middleware that will handle every request sent to our application. Most of these features
in the default configuration were scattered across the web forms configuration files and are now in one place for
ease of reference.
No longer is the configuration of the custom error page placed in a web.config file, but now is configured to
always be shown if the application environment is not labeled Development . Additionally, ASP.NET Core
applications are now configured to serve secure pages with TLS by default with the UseHttpsRedirection
method call.
Next, an unexpected configuration method is listed to UseStaticFiles . In ASP.NET Core, support for requests for
static files (like JavaScript, CSS, and image files) must be explicitly enabled, and only files in the app's wwwroot
folder are publicly addressable by default.
The next line is the first that replicates one of the configuration options from web forms: UseRouting . This
method adds the ASP.NET Core router to the pipeline and it can be either configured here or in the individual
files that it can consider routing to. More information about routing configuration can be found in the Routing
section.
The final statement in this method defines the endpoints that ASP.NET Core is listening on. These routes are the
web accessible locations that you can access on the web server and receive some content handled by .NET and
returned to you. The first entry, MapBlazorHub configures a SignalR hub for use in providing the real-time and
persistent connection to the server where the state and rendering of Blazor components is handled. The
MapFallbackToPage method call indicates the web-accessible location of the page that starts the Blazor
application and also configures the application to handle deep-linking requests from the client-side. You will see
this feature at work if you open a browser and navigate directly to Blazor handled route in your application, such
as /counter in the default project template. The request gets handled by the _Host.cshtml fallback page, which
then runs the Blazor router and renders the counter page.
More details about both strategies to manage your CSS and JavaScript files are available in the Bundle and
minify static assets in ASP.NET Core documentation.
PR EVIO U S NE XT
Build reusable UI components with Blazor
3/6/2021 • 16 minutes to read • Edit Online
One of the beautiful things about ASP.NET Web Forms is how it enables encapsulation of reusable pieces of user
interface (UI) code into reusable UI controls. Custom user controls can be defined in markup using .ascx files.
You can also build elaborate server controls in code with full designer support.
Blazor also supports UI encapsulation through components. A component:
Is a self-contained chunk of UI.
Maintains its own state and rendering logic.
Can define UI event handlers, bind to input data, and manage its own lifecycle.
Is typically defined in a .razor file using Razor syntax.
An introduction to Razor
Razor is a light-weight markup templating language based on HTML and C#. With Razor, you can seamlessly
transition between markup and C# code to define your component rendering logic. When the .razor file is
compiled, the rendering logic is captured in a structured way in a .NET class. The name of the compiled class is
taken from the .razor file name. The namespace is taken from the default namespace for the project and the
folder path, or you can explicitly specify the namespace using the @namespace directive (more on Razor
directives below).
A component's rendering logic is authored using normal HTML markup with dynamic logic added using C#. The
@ character is used to transition to C#. Razor is typically smart about figuring out when you've switched back
to HTML. For example, the following component renders a <p> tag with the current time:
<p>@DateTime.Now</p>
<p>@(DateTime.Now)</p>
Razor also makes it easy to use C# control flow in your rendering logic. For example, you can conditionally
render some HTML like this:
@if (value % 2 == 0)
{
<p>The value was even.</p>
}
Or you can generate a list of items using a normal C# foreach loop like this:
<ul>
@foreach (var item in items)
{
<li>@item.Text</li>
}
</ul>
Razor directives, like directives in ASP.NET Web Forms, control many aspects of how a Razor component is
compiled. Examples include the component's:
Namespace
Base class
Implemented interfaces
Generic parameters
Imported namespaces
Routes
Razor directives start with the @ character and are typically used at the start of a new line at the start of the file.
For example, the @namespace directive defines the component's namespace:
@namespace MyComponentNamespace
The following table summarizes the various Razor directives used in Blazor and their ASP.NET Web Forms
equivalents, if they exist.
@page Specifies the route for the @page "/product/{id}" <%@ Page %>
component
Razor components also make extensive use of directive attributes on elements to control various aspects of how
components get compiled (event handling, data binding, component & element references, and so on). Directive
attributes all follow a common generic syntax where the values in parenthesis are optional:
@directive(-suffix(:name))(="value")
The following table summarizes the various attributes for Razor directives used in Blazor.
The various directive attributes used by Blazor ( @onclick , @bind , @ref , and so on) are covered in the sections
below and later chapters.
Many of the syntaxes used in .aspx and .ascx files have parallel syntaxes in Razor. Below is a simple comparison
of the syntaxes for ASP.NET Web Forms and Razor.
F EAT URE W EB F O RM S SY N TA X RA Z O R SY N TA X
To add members to the Razor component class, use the @code directive. This technique is similar to using a
<script runat="server">...</script> block in an ASP.NET Web Forms user control or page.
@code {
int count = 0;
void IncrementCount()
{
count++;
}
}
Because Razor is based on C#, it must be compiled from within a C# project (.csproj). You can't compile .razor
files from a Visual Basic project (.vbproj). You can still reference Visual Basic projects from your Blazor project.
The opposite is true too.
For a full Razor syntax reference, see Razor syntax reference for ASP.NET Core.
Use components
Aside from normal HTML, components can also use other components as part of their rendering logic. The
syntax for using a component in Razor is similar to using a user control in an ASP.NET Web Forms app.
Components are specified using an element tag that matches the type name of the component. For example,
you can add a Counter component like this:
<Counter />
@using MyComponentLib
<Counter />
As seen in the default Blazor projects, it's common to put @using directives into a _Imports.razor file so that
they're imported into all .razor files in the same directory and in child directories.
If the namespace for a component isn't in scope, you can specify a component using its full type name, as you
can in C#:
<MyComponentLib.Counter />
Component parameters
In ASP.NET Web Forms, you can flow parameters and data to controls using public properties. These properties
can be set in markup using attributes or set directly in code. Blazor components work in a similar fashion,
although the component properties must also be marked with the [Parameter] attribute to be considered
component parameters.
The following Counter component defines a component parameter called IncrementAmount that can be used to
specify the amount that the Counter should be incremented each time the button is clicked.
<h1>Counter</h1>
@code {
int currentCount = 0;
[Parameter]
public int IncrementAmount { get; set; } = 1;
void IncrementCount()
{
currentCount+=IncrementAmount;
}
}
To specify a component parameter in Blazor, use an attribute as you would in ASP.NET Web Forms:
Event handlers
Both ASP.NET Web Forms and Blazor provide an event-based programming model for handling UI events.
Examples of such events include button clicks and text input. In ASP.NET Web Forms, you use HTML server
controls to handle UI events exposed by the DOM, or you can handle events exposed by web server controls.
The events are surfaced on the server through form post-back requests. Consider the following Web Forms
button click example:
Counter.ascx
Counter.ascx.cs
In Blazor, you can register handlers for DOM UI events directly using directive attributes of the form @on{event} .
The {event} placeholder represents the name of the event. For example, you can listen for button clicks like
this:
@code {
void OnClick()
{
Console.WriteLine("The button was clicked!);
}
}
Event handlers can accept an optional, event-specific argument to provide more information about the event.
For example, mouse events can take a MouseEventArgs argument, but it isn't required.
@code {
void OnClick(MouseEventArgs e)
{
Console.WriteLine($"Mouse clicked at {e.ScreenX}, {e.ScreenY}.");
}
}
Instead of referring to a method group for an event handler, you can use a lambda expression. A lambda
expression allows you to close over other in-scope values.
Event handlers can execute synchronously or asynchronously. For example, the following OnClick event
handler executes asynchronously:
@code {
async Task OnClick()
{
var result = await Http.GetAsync("api/values");
}
}
After an event is handled, the component is rendered to account for any component state changes. With
asynchronous event handlers, the component is rendered immediately after the handler execution completes.
The component is rendered again after the asynchronous Task completes. This asynchronous execution mode
provides an opportunity to render some appropriate UI while the asynchronous Task is still in progress.
<button @onclick="ShowMessage">Get message</button>
@if (showMessage)
{
@if (message == null)
{
<p><em>Loading...</em></p>
}
else
{
<p>The message is: @message</p>
}
}
@code
{
bool showMessage = false;
string message;
Components can also define their own events by defining a component parameter of type
EventCallback<TValue> . Event callbacks support all the variations of DOM UI event handlers: optional
arguments, synchronous or asynchronous, method groups, or lambda expressions.
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
}
Data binding
Blazor provides a simple mechanism to bind data from a UI component to the component's state. This approach
differs from the features in ASP.NET Web Forms for binding data from data sources to UI controls. We'll cover
handling data from different data sources in the Dealing with data section.
To create a two-way data binding from a UI component to the component's state, use the @bind directive
attribute. In the following example, the value of the check box is bound to the isChecked field.
@code {
bool isChecked;
}
When the component is rendered, the value of the checkbox is set to the value of the isChecked field. When the
user toggles the checkbox, the onchange event is fired and the isChecked field is set to the new value. The
@bind syntax in this case is equivalent to the following markup:
@code {
string text;
}
Components can also support data binding to their parameters. To data bind, define an event callback parameter
with the same name as the bindable parameter. The "Changed" suffix is added to the name.
PasswordBox.razor
Password: <input
value="@Password"
@oninput="OnPasswordChanged"
type="@(showPassword ? "text" : "password")" />
@code {
private bool showPassword;
[Parameter]
public string Password { get; set; }
[Parameter]
public EventCallback<string> PasswordChanged { get; set; }
To chain a data binding to an underlying UI element, set the value and handle the event directly on the UI
element instead of using the @bind attribute.
To bind to a component parameter, use a @bind-{Parameter} attribute to specify the parameter to which you
want to bind.
@code {
string password;
}
State changes
If the component's state has changed outside of a normal UI event or event callback, then the component must
manually signal that it needs to be rendered again. To signal that a component's state has changed, call the
StateHasChanged method on the component.
In the example below, a component displays a message from an AppState service that can be updated by other
parts of the app. The component registers its StateHasChanged method with the AppState.OnChange event so
that the component is rendered whenever the message gets updated.
public class AppState
{
public string Message { get; }
@code {
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged
}
}
Component lifecycle
The ASP.NET Web Forms framework has well-defined lifecycle methods for modules, pages, and controls. For
example, the following control implements event handlers for the Init , Load , and UnLoad lifecycle events:
Counter.ascx.cs
Blazor components also have a well-defined lifecycle. A component's lifecycle can be used to initialize
component state and implement advanced component behaviors.
All of Blazor's component lifecycle methods have both synchronous and asynchronous versions. Component
rendering is synchronous. You can't run asynchronous logic as part of the component rendering. All
asynchronous logic must execute as part of an async lifecycle method.
OnInitialized
The OnInitialized and OnInitializedAsync methods are used to initialize the component. A component is
typically initialized after it's first rendered. After a component is initialized, it may be rendered multiple times
before it's eventually disposed. The OnInitialized method is similar to the Page_Load event in ASP.NET Web
Forms pages and controls.
OnAfterRender
The OnAfterRender and OnAfterRenderAsync methods are called after a component has finished rendering.
Element and component references are populated at this point (more on these concepts below). Interactivity
with the browser is enabled at this point. Interactions with the DOM and JavaScript execution can safely take
place.
@using System
@implements IDisposable
...
@code {
public void Dispose()
{
...
}
}
@code {
MyLoginDialog loginDialog;
void OnSomething()
{
loginDialog.Show();
}
}
When the parent component is rendered, the field is populated with the child component instance. You can then
call methods on, or otherwise manipulate, the component instance.
Manipulating component state directly using component references isn't recommended. Doing so prevents the
component from being rendered automatically at the correct times.
Templated components
In ASP.NET Web Forms, you can create templated controls. Templated controls enable the developer to specify a
portion of the HTML used to render a container control. The mechanics of building templated server controls
are complex, but they enable powerful scenarios for rendering data in a user customizable way. Examples of
templated controls include Repeater and DataList .
Blazor components can also be templated by defining component parameters of type RenderFragment or
RenderFragment<T> . A RenderFragment represents a chunk of Razor markup that can then be rendered by the
component. A RenderFragment<T> is a chunk of Razor markup that takes a parameter that can be specified when
the render fragment is rendered.
Child content
Blazor components can capture their child content as a RenderFragment and render that content as part of the
component rendering. To capture child content, define a component parameter of type RenderFragment and
name it ChildContent .
ChildContentComponent.razor
<div>@ChildContent</div>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
A parent component can then supply child content using normal Razor syntax.
<ChildContentComponent>
<ChildContent>
<p>The time is @DateTime.Now</p>
</ChildContent>
</ChildContentComponent>
Template parameters
A templated Blazor component can also define multiple component parameters of type RenderFragment or
RenderFragment<T> . The parameter for a RenderFragment<T> can be specified when it's invoked. To specify a
generic type parameter for a component, use the @typeparam Razor directive.
SimpleListView.razor
@typeparam TItem
@Heading
<ul>
@foreach (var item in Items)
{
<li>@ItemTemplate(item)</li>
}
</ul>
@code {
[Parameter]
public RenderFragment Heading { get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate { get; set; }
[Parameter]
public IEnumerable<TItem> Items { get; set; }
}
When using a templated component, the template parameters can be specified using child elements that match
the names of the parameters. Component arguments of type RenderFragment<T> passed as elements have an
implicit parameter named context . You can change the name of this implement parameter using the Context
attribute on the child element. Any generic type parameters can be specified using an attribute that matches the
name of the type parameter. The type parameter will be inferred if possible:
<h1>My list</h1>
<ul>
<li><p>The message is: message1</p></li>
<li><p>The message is: message2</p></li>
<ul>
Code-behind
A Blazor component is typically authored in a single .razor file. However, it's also possible to separate the code
and markup using a code-behind file. To use a component file, add a C# file that matches the file name of the
component file but with a .cs extension added (Counter.razor.cs). Use the C# file to define a base class for the
component. You can name the base class anything you'd like, but it's common to name the class the same as the
component class, but with a Base extension added ( CounterBase ). The component-based class must also derive
from ComponentBase . Then, in the Razor component file, add the @inherits directive to specify the base class for
the component ( @inherits CounterBase ).
Counter.razor
@inherits CounterBase
<h1>Counter</h1>
Counter.razor.cs
The visibility of the component's members in the base class must be protected or public to be visible to the
component class.
Additional resources
The preceding isn't an exhaustive treatment of all aspects of Blazor components. For more information on how
to Create and use ASP.NET Core Razor components, see the Blazor documentation.
PR EVIO U S NE XT
Pages, routing, and layouts
11/2/2020 • 6 minutes to read • Edit Online
ASP.NET Web Forms apps are composed of pages defined in .aspx files. Each page's address is based on its
physical file path in the project. When a browser makes a request to the page, the contents of the page are
dynamically rendered on the server. The rendering accounts for both the page's HTML markup and its server
controls.
In Blazor, each page in the app is a component, typically defined in a .razor file, with one or more specified
routes. Routing mostly happens client-side without involving a specific server request. The browser first makes a
request to the root address of the app. A root Router component in the Blazor app then handles intercepting
navigation requests and them to the correct component.
Blazor also supports deep linking. Deep linking occurs when the browser makes a request to a specific route
other than the root of the app. Requests for deep links sent to the server are routed to the Blazor app, which
then routes the request client-side to the correct component.
A simple page in ASP.NET Web Forms might contain the following markup:
Name.aspx
Name.aspx.cs
<div>
What is your name?<br />
<input @bind="text" />
<button @onclick="OnClick">Submit</button>
</div>
<div>
@if (name != null)
{
@:Hello @name
}
</div>
@code {
string text;
string name;
void OnClick() {
name = text;
}
}
Create pages
To create a page in Blazor, create a component and add the @page Razor directive to specify the route for the
component. The @page directive takes a single parameter, which is the route template to add to that component.
@page "/counter"
The route template parameter is required. Unlike ASP.NET Web Forms, the route to a Blazor component isn't
inferred from its file location (although that may be a feature added in the future).
The route template syntax is the same basic syntax used for routing in ASP.NET Web Forms. Route parameters
are specified in the template using braces. Blazor will bind route values to component parameters with the same
name (case-insensitive).
@page "/product/{id}"
<h1>Product @Id</h1>
@code {
[Parameter]
public string Id { get; set; }
}
You can also specify constraints on the value of the route parameter. For example, to constrain the product ID to
be an int :
@page "/product/{id:int}"
<h1>Product @Id</h1>
@code {
[Parameter]
public int Id { get; set; }
}
For a full list of the route constraints supported by Blazor, see Route constraints.
Router component
Routing in Blazor is handled by the Router component. The Router component is typically used in the app's
root component (App.razor).
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
The Router component discovers the routable components in the specified AppAssembly and in the optionally
specified AdditionalAssemblies . When the browser navigates, the Router intercepts the navigation and renders
the contents of its Found parameter with the extracted RouteData if a route matches the address, otherwise the
Router renders its NotFound parameter.
The RouteView component handles rendering the matched component specified by the RouteData with its
layout if it has one. If the matched component doesn't have a layout, then the optionally specified DefaultLayout
is used.
The LayoutView component renders its child content within the specified layout. We'll look at layouts more in
detail later in this chapter.
Navigation
In ASP.NET Web Forms, you trigger navigation to a different page by returning a redirect response to the
browser. For example:
Returning a redirect response isn't typically possible in Blazor. Blazor doesn't use a request-reply model. You can,
however, trigger browser navigations directly, as you can with JavaScript.
Blazor provides a NavigationManager service that can be used to:
Get the current browser address
Get the base address
Trigger navigations
Get notified when the address changes
To navigate to a different address, use the NavigateTo method:
@page "/"
@inject NavigationManager NavigationManager
<button @onclick="Navigate">Navigate</button>
@code {
void Navigate() {
NavigationManager.NavigateTo("counter");
}
}
For a description of all NavigationManager members, see URI and navigation state helpers.
Base URLs
If your Blazor app is deployed under a base path, then you need to specify the base URL in the page metadata
using the <base> tag for routing to work property. If the host page for the app is server-rendered using Razor,
then you can use the ~/ syntax to specify the app's base address. If the host page is static HTML, then you need
to specify the base URL explicitly.
Page layout
Page layout in ASP.NET Web Forms is handled by Master Pages. Master Pages define a template with one or
more content placeholders that can then be supplied by individual pages. Master Pages are defined in .master
files and start with the <%@ Master %> directive. The content of the .master files is coded as you would an .aspx
page, but with the addition of <asp:ContentPlaceHolder> controls to mark where pages can supply content.
Site.master
<!DOCTYPE html>
<html lang="en">
<head runat="server">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%: Page.Title %> - My ASP.NET Application</title>
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
</head>
<body>
<form runat="server">
<div class="container body-content">
<asp:ContentPlaceHolder ID="MainContent" runat="server">
</asp:ContentPlaceHolder>
<hr />
<footer>
<p>© <%: DateTime.Now.Year %> - My ASP.NET Application</p>
</footer>
</div>
</form>
</body>
</html>
In Blazor, you handle page layout using layout components. Layout components inherit from
LayoutComponentBase , which defines a single Body property of type RenderFragment , which can be used to
render the contents of the page.
MainLayout.razor
@inherits LayoutComponentBase
<h1>Main layout</h1>
<div>
@Body
</div>
When the page with a layout is rendered, the page is rendered within the contents of the specified layout at the
location where the layout renders its Body property.
To apply a layout to a page, use the @layout directive:
@layout MainLayout
You can specify the layout for all components in a folder and subfolders using an _Imports.razor file. You can
also specify a default layout for all your pages using the Router component.
Master Pages can define multiple content placeholders, but layouts in Blazor only have a single Body property.
This limitation of Blazor layout components will hopefully be addressed in a future release.
Master Pages in ASP.NET Web Forms can be nested. That is, a Master Page may also use a Master Page. Layout
components in Blazor may be nested too. You can apply a layout component to a layout component. The
contents of the inner layout will be rendered within the outer layout.
ChildLayout.razor
@layout MainLayout
<h2>Child layout</h2>
<div>
@Body
</div>
Index.razor
@page "/"
@layout ChildLayout
<p>I'm in a nested layout!</p>
<h1>Main layout</h1>
<div>
<h2>Child layout</h2>
<div>
<p>I'm in a nested layout!</p>
</div>
</div>
Layouts in Blazor don't typically define the root HTML elements for a page ( <html> , <body> , <head> , and so
on). The root HTML elements are instead defined in a Blazor app's host page, which is used to render the initial
HTML content for the app (see Bootstrap Blazor). The host page can render multiple root components for the
app with surrounding markup.
Components in Blazor, including pages, can't render <script> tags. This rendering restriction exists because
<script> tags get loaded once and then can't be changed. Unexpected behavior may occur if you try to render
the tags dynamically using Razor syntax. Instead, all <script> tags should be added to the app's host page.
PR EVIO U S NE XT
State management
11/2/2020 • 4 minutes to read • Edit Online
State management is a key concept of Web Forms applications, facilitated through View State, Session State,
Application State, and Postback features. These stateful features of the framework helped to hide the state
management required for an application and allow application developers to focus on delivering their
functionality. With ASP.NET Core and Blazor, some of these features have been relocated and some have been
removed altogether. This chapter reviews how to maintain state and deliver the same functionality with the new
features in Blazor.
Application state
The Application object in the Web Forms framework provides a massive, cross-request repository for
interacting with application-scope configuration and state. Application state was an ideal place to store various
application configuration properties that would be referenced by all requests, regardless of the user making the
request. The problem with the Application object was that data didn't persist across multiple servers. The state
of the application object was lost between restarts.
As with Session , it's recommended that data move to a persistent backing store that could be accessed by
multiple server instances. If there is volatile data that you would like to be able to access across requests and
users, you could easily store it in a singleton service that can be injected into components that require this
information or interaction.
The construction of an object to maintain application state and its consumption could resemble the following
implementation:
app.AddSingleton<MyApplicationState>();
The MyApplicationState object is created only once on the server, and the value VisitorCounter is fetched and
output in the component's label. The VisitorCounter value should be persisted and retrieved from a backing
data store for durability and scalability.
In the browser
Application data can also be stored client-side on the user's device so that is available later. There are two
browser features that allow for persistence of data in different scopes of the user's browser:
localStorage - scoped to the user's entire browser. If the page is reloaded, the browser is closed and
reopened, or another tab is opened with the same URL then the same localStorage is provided by the
browser
sessionStorage - scoped to the user's current browser tab. If the tab is reloaded, the state persists. However,
if the user opens another tab to your application or closes and reopens the browser the state is lost.
You can write some custom JavaScript code to interact with these features, or there are a number of NuGet
packages that you can use that provide this functionality. One such package is
Microsoft.AspNetCore.ProtectedBrowserStorage.
For instructions on utilizing this package to interact with localStorage and sessionStorage , see the Blazor State
Management article.
PR EVIO U S NE XT
Forms and validation
11/2/2020 • 3 minutes to read • Edit Online
The ASP.NET Web Forms framework includes a set of validation server controls that handle validating user input
entered into a form ( RequiredFieldValidator , CompareValidator , RangeValidator , and so on). The ASP.NET Web
Forms framework also supports model binding and validating the model based on data annotations (
[Required] , [StringLength] , [Range] , and so on). The validation logic can be enforced both on the server and
on the client using unobtrusive JavaScript-based validation. The ValidationSummary server control is used to
display a summary of the validation errors to the user.
Blazor supports the sharing of validation logic between both the client and the server. ASP.NET provides pre-
built JavaScript implementations of many common server validations. In many cases, the developer still has to
write JavaScript to fully implement their app-specific validation logic. The same model types, data annotations,
and validation logic can be used on both the server and client.
Blazor provides a set of input components. The input components handle binding field data to a model and
validating the user input when the form is submitted.
IN P UT C O M P O N EN T REN DERED H T M L EL EM EN T
InputSelect <select>
InputText <input>
InputTextArea <textarea>
The EditForm component wraps these input components and orchestrates the validation process through an
EditContext . When creating an EditForm , you specify what model instance to bind to using the Model
parameter. Validation is typically done using data annotations, and it's extensible. To enable data annotation-
based validation, add the DataAnnotationsValidator component as a child of the EditForm . The EditForm
component provides a convenient event for handling valid ( OnValidSubmit ) and invalid ( OnInvalidSubmit )
submissions. There's also a more generic OnSubmit event that lets you trigger and handle the validation
yourself.
To display a validation error summary, use the ValidationSummary component. To display validation messages
for a specific input field, use the ValidationMessage component, specifying a lambda expression for the For
parameter that points to the appropriate model member.
The following model type defines several validation rules using data annotations:
using System;
using System.ComponentModel.DataAnnotations;
[Required]
public string Classification { get; set; }
[Range(1, 100000,
ErrorMessage = "Accommodation invalid (1-100000).")]
public int MaximumAccommodation { get; set; }
[Required]
[Range(typeof(bool), "true", "true",
ErrorMessage = "This form disallows unapproved ships.")]
public bool IsValidatedDesign { get; set; }
[Required]
public DateTime ProductionDate { get; set; }
}
The following component demonstrates building a form in Blazor based on the Starship model type:
<h1>New Ship Entry Form</h1>
<p>
<label for="identifier">Identifier: </label>
<InputText id="identifier" @bind-Value="starship.Identifier" />
<ValidationMessage For="() => starship.Identifier" />
</p>
<p>
<label for="description">Description (optional): </label>
<InputTextArea id="description" @bind-Value="starship.Description" />
</p>
<p>
<label for="classification">Primary Classification: </label>
<InputSelect id="classification" @bind-Value="starship.Classification">
<option value="">Select classification ...</option>
<option value="Exploration">Exploration</option>
<option value="Diplomacy">Diplomacy</option>
<option value="Defense">Defense</option>
</InputSelect>
<ValidationMessage For="() => starship.Classification" />
</p>
<p>
<label for="accommodation">Maximum Accommodation: </label>
<InputNumber id="accommodation" @bind-Value="starship.MaximumAccommodation" />
<ValidationMessage For="() => starship.MaximumAccommodation" />
</p>
<p>
<label for="valid">Engineering Approval: </label>
<InputCheckbox id="valid" @bind-Value="starship.IsValidatedDesign" />
<ValidationMessage For="() => starship.IsValidatedDesign" />
</p>
<p>
<label for="productionDate">Production Date: </label>
<InputDate id="productionDate" @bind-Value="starship.ProductionDate" />
<ValidationMessage For="() => starship.ProductionDate" />
</p>
<button type="submit">Submit</button>
</EditForm>
@code {
private Starship starship = new Starship();
After the form submission, the model-bound data hasn't been saved to any data store, like a database. In a
Blazor WebAssembly app, the data must be sent to the server. For example, using an HTTP POST request. In a
Blazor Server app, the data is already on the server, but it must be persisted. Handling data access in Blazor apps
is the subject of the Dealing with data section.
Additional resources
For more information on forms and validation in Blazor apps, see the Blazor documentation.
PR EVIO U S NE XT
Work with data
3/6/2021 • 5 minutes to read • Edit Online
Data access is the backbone of an ASP.NET Web Forms app. If you're building forms for the web, what happens
to that data? With Web Forms, there were multiple data access techniques you could use to interact with a
database:
Data Sources
ADO.NET
Entity Framework
Data Sources were controls that you could place on a Web Forms page and configure like other controls. Visual
Studio provided a friendly set of dialogs to configure and bind the controls to your Web Forms pages.
Developers who enjoy a "low code" or "no code" approach preferred this technique when Web Forms was first
released.
ADO.NET is the low-level approach to interacting with a database. Your apps could create a connection to the
database with Commands, Recordsets, and Datasets for interacting. The results could then be bound to fields on
screen without much code. The drawback of this approach was that each set of ADO.NET objects ( Connection ,
Command , and Recordset ) was bound to libraries provided by a database vendor. Use of these components
made the code rigid and difficult to migrate to a different database.
Entity Framework
Entity Framework (EF) is the open source object-relational mapping framework maintained by the .NET
Foundation. Initially released with .NET Framework, EF allows for generating code for the database connections,
storage schemas, and interactions. With this abstraction, you can focus on your app's business rules and allow
the database to be managed by a trusted database administrator. In .NET, you can use an updated version of EF
called EF Core. EF Core helps generate and maintain the interactions between your code and the database with a
series of commands that are available for you using the dotnet ef command-line tool. Let's take a look at a few
samples to get you working with a database.
EF Code First
A quick way to get started building your database interactions is to start with the class objects you want to work
with. EF provides a tool to help generate the appropriate database code for your classes. This approach is called
"Code First" development. Consider the following Product class for a sample storefront app that we want to
store in a relational database like Microsoft SQL Server.
public class Product
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[MaxLength(4000)]
public string Description { get; set; }
[Range(0, 99999,99)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
}
Product has a primary key and three additional fields that would be created in our database:
EF will identify the Id property as a primary key by convention.
Name will be stored in a column configured for text storage. The [Required] attribute decorating this
property will add a not null constraint to help enforce this declared behavior of the property.
Description will be stored in a column configured for text storage, and have a maximum length configured
of 4000 characters as dictated by the [MaxLength] attribute. The database schema will be configured with a
column named MaxLength using data type varchar(4000) .
The Price property will be stored as currency. The [Range] attribute will generate appropriate constraints
to prevent data storage outside of the minimum and maximum values declared.
We need to add this Product class to a database context class that defines the connection and translation
operations with our database.
The MyDbContext class provides the one property that defines the access and translation for the Product class.
Your application configures this class for interaction with the database using the following entries in the
Startup class's ConfigureServices method:
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer("MY DATABASE CONNECTION STRING"));
The preceding code will connect to a SQL Server database with the specified connection string. You can place the
connection string in your appsettings.json file, environment variables, or other configuration storage locations
and replace this embedded string appropriately.
You can then generate the database table appropriate for this class using the following commands:
The first command defines the changes you're making to the database schema as a new EF Migration called
Create Product table . A Migration defines how to apply and remove your new database changes.
Once applied, you have a simple Product table in your database and some new classes added to the project
that help manage the database schema. You can find these generated classes, by default, in a new folder called
Migrations. When you make changes to the Product class or add more related classes you would like
interacting with your database, you need to run the command-line commands again with a new name of the
migration. This command will generate another set of migration classes to update your database schema.
EF Database First
For existing databases, you can generate the classes for EF Core by using the .NET command-line tools. To
scaffold the classes, use a variation of the following command:
The preceding command connects to the database using the specified connection string and the
Microsoft.EntityFrameworkCore.SqlServer provider. Once connected, a database context class named
MyDbContext is created. Additionally, supporting classes are created for the Product and Customer tables that
were specified with the -t options. There are many configuration options for this command to generate the
class hierarchy appropriate for your database. For a complete reference, see the command's documentation.
More information about EF Core can be found on the Microsoft Docs site.
Whenever you need to access data from GitHub, create a client with a name of github . The client is configured
with the base address, and the request headers are set appropriately. Inject the IHttpClientFactory into your
Blazor components with the @inject directive or an [Inject] attribute on a property. Create your named client
and interact with services using the following syntax:
...
@code {
protected override async Task OnInitializedAsync()
{
var client = factory.CreateClient("github");
var response = await client.GetAsync("repos/dotnet/docs/issues");
response.EnsureStatusCode();
var content = await response.Content.ReadAsStringAsync();
}
}
This method returns the string describing the collection of issues in the dotnet/docs GitHub repository. It returns
content in JSON format and is deserialized into appropriate GitHub issue objects. There are many ways that you
can configure the HttpClientFactory to deliver preconfigured HttpClient objects. Try configuring multiple
HttpClient instances with different names and endpoints for the various web services you work with. This
approach will make your interactions with those services easier to work with on each page. For more details,
read the documentation for the IHttpClientFactory.
PR EVIO U S NE XT
Modules, handlers, and middleware
3/6/2021 • 2 minutes to read • Edit Online
An ASP.NET Core app is built upon a series of middleware. Middleware is handlers that are arranged into a
pipeline to handle requests and responses. In a Web Forms app, HTTP handlers and modules solve similar
problems. In ASP.NET Core, modules, handlers, Global.asax.cs, and the app life cycle are replaced with
middleware. In this chapter, you'll learn about middleware in the context of a Blazor app.
Overview
The ASP.NET Core request pipeline consists of a sequence of request delegates, called one after the other. The
following diagram demonstrates the concept. The thread of execution follows the black arrows.
The preceding diagram lacks a concept of lifecycle events. This concept is foundational to how ASP.NET Web
Forms requests are handled. This system makes it easier to reason about what process is occurring and allows
middleware to be inserted at any point. Middleware executes in the order in which it's added to the request
pipeline. They're also added in code instead of configuration files, usually in Startup.cs.
Katana
Readers familiar with Katana will feel comfortable in ASP.NET Core. In fact, Katana is a framework from which
ASP.NET Core derives. It introduced similar middleware and pipeline patterns for ASP.NET 4.x. Middleware
designed for Katana can be adapted to work with the ASP.NET Core pipeline.
Common middleware
ASP.NET 4.x includes many modules. In a similar fashion, ASP.NET Core has many middleware components
available as well. IIS modules may be used in some cases with ASP.NET Core. In other cases, native ASP.NET Core
middleware may be available.
The following table lists replacement middleware and components in ASP.NET Core.
M O DUL E A SP. N ET 4. X M O DUL E A SP. N ET C O RE O P T IO N
This list isn't exhaustive but should give an idea of what mapping exists between the two frameworks. For a
more detailed list, see IIS modules with ASP.NET Core.
Custom middleware
Built-in middleware may not handle all scenarios needed for an app. In such cases, it makes sense to create your
own middleware. There are multiple ways of defining middleware, with the simplest being a simple delegate.
Consider the following middleware, which accepts a culture request from a query string:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
Middleware can also be defined as class, either by implementing the IMiddleware interface or by following
middleware convention. For more information, see Write custom ASP.NET Core middleware.
PR EVIO U S NE XT
App configuration
3/6/2021 • 6 minutes to read • Edit Online
The primary way to load app configuration in Web Forms is with entries in the web.config file—either on the
server or a related configuration file referenced by web.config. You can use the static ConfigurationManager
object to interact with app settings, data repository connection strings, and other extended configuration
providers that are added into the app. It's typical to see interactions with app configuration as seen in the
following code:
With ASP.NET Core and server-side Blazor, the web.config file MAY be present if your app is hosted on a
Windows IIS server. However, there's no ConfigurationManager interaction with this configuration, and you can
receive more structured app configuration from other sources. Let's take a look at how configuration is gathered
and how you can still access configuration information from a web.config file.
Configuration sources
ASP.NET Core recognizes there are many configuration sources you may want to use for your app. The
framework attempts to offer you the best of these features by default. Configuration is read and aggregated
from these various sources by ASP.NET Core. Later loaded values for the same configuration key take
precedence over earlier values.
ASP.NET Core was designed to be cloud-aware and to make the configuration of apps easier for both operators
and developers. ASP.NET Core is environment-aware and knows if it's running in your Production or
Development environment. The environment indicator is set in the ASPNETCORE_ENVIRONMENT system environment
variable. If no value is configured, the app defaults to running in the Production environment.
Your app can trigger and add configuration from several sources based on the environment's name. By default,
the configuration is loaded from the following resources in the order listed:
1. appsettings.json file, if present
2. appsettings.{ENVIRONMENT_NAME}.json file, if present
3. User secrets file on disk, if present
4. Environment variables
5. Command-line arguments
When presented with the preceding JSON, the configuration system flattens child values and references their
fully qualified hierarchical paths. A colon ( : ) character separates each property in the hierarchy. For example,
the configuration key section1:key0 accesses the section1 object literal's key0 value.
User secrets
User secrets are:
Configuration values that are stored in a JSON file on the developer's workstation, outside of the app
development folder.
Only loaded when running in the Development environment.
Associated with a specific app.
Managed with the .NET CLI's user-secrets command.
Configure your app for secrets storage by executing the user-secrets command:
The preceding command adds a UserSecretsId element to the project file. The element contains a GUID, which
is used to associate secrets with the app. You can then define a secret with the set command. For example:
The preceding command makes the Parent:ApiKey configuration key available on a developer's workstation
with the value 12345 .
For more information about creating, storing, and managing user secrets, see the Safe storage of app secrets in
development in ASP.NET Core document.
Environment variables
The next set of values loaded into your app configuration is the system's environment variables. All of your
system's environment variable settings are now accessible to you through the configuration API. Hierarchical
values are flattened and separated by colon characters when read inside your app. However, some operating
systems don't allow the colon character environment variable names. ASP.NET Core addresses this limitation by
converting values that have double-underscores ( __ ) into a colon when they're accessed. The Parent:ApiKey
value from the user secrets section above can be overridden with the environment variable Parent__ApiKey .
Command-line arguments
Configuration can also be provided as command-line arguments when your app is started. Use the double-dash
( -- ) or forward-slash ( / ) notation to indicate the name of the configuration value to set and the value to be
configured. The syntax resembles the following commands:
<aspNetCore processPath="dotnet"
arguments=".\MyApp.dll"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="Parent:ApiKey" value="67890" />
</environmentVariables>
</aspNetCore>
This preceding statement makes the IConfiguration object available as the Configuration variable throughout
the rest of the Razor template.
Individual configuration settings can be read by specifying the configuration setting hierarchy sought as an
indexer parameter:
You can fetch entire configuration sections by using the GetSection method to retrieve a collection of keys at a
specific location with a syntax similar to GetSection("section1") to retrieve the configuration for section1 from
the earlier example.
This class hierarchy can be populated by adding the following line to the Startup.ConfigureServices method:
services.Configure<MyConfig>(Configuration);
In the rest of the app, you can add an input parameter to classes or an @inject directive in Razor templates of
type IOptions<MyConfig> to receive the strongly typed configuration settings. The IOptions<MyConfig>.Value
property will yield the MyConfig value populated from the configuration settings.
More information about the Options feature can be found in the Options pattern in ASP.NET Core document.
PR EVIO U S NE XT
Security: Authentication and Authorization in
ASP.NET Web Forms and Blazor
3/6/2021 • 14 minutes to read • Edit Online
Migrating from an ASP.NET Web Forms application to Blazor will almost certainly require updating how
authentication and authorization are performed, assuming the application had authentication configured. This
chapter will cover how to migrate from the ASP.NET Web Forms universal provider model (for membership,
roles, and user profiles) and how to work with ASP.NET Core Identity from Blazor apps. While this chapter will
cover the high-level steps and considerations, the detailed steps and scripts may be found in the referenced
documentation.
The universal provider handles users, membership, roles, and profiles. Users are assigned globally unique
identifiers and basic information like userId, userName etc. are stored in the aspnet_Users table. Authentication
information, such as password, password format, password salt, lockout counters and details, etc. are stored in
the aspnet_Membership table. Roles consist simply of names and unique identifiers, which are assigned to users
via the aspnet_UsersInRoles association table, providing a many-to-many relationship.
If your existing system is using roles in addition to membership, you will need to migrate the user accounts, the
associated passwords, the roles, and the role membership into ASP.NET Core Identity. You will also most likely
need to update your code where you're currently performing role checks using if statements to instead leverage
declarative filters, attributes, and/or tag helpers. We will review migration considerations in greater detail at the
end of this chapter.
Authorization configuration in Web Forms
To configure authorized access to certain pages in an ASP.NET Web Forms application, typically you specify that
certain pages or folders are inaccessible to anonymous users. This configuration is done in the web.config file:
<?xml version="1.0"?>
<configuration>
<system.web>
<authentication mode="Forms">
<forms defaultUrl="~/home.aspx" loginUrl="~/login.aspx"
slidingExpiration="true" timeout="2880"></forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</configuration>
The authenticationconfiguration section sets up the forms authentication for the application. The
authorization section is used to disallow anonymous users for the entire application. However, you can provide
more granular authorization rules on a per-location basis as well as apply role-based authorization checks.
<location path="login.aspx">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
The above configuration, when combined with the first one, would allow anonymous users to access the login
page, overriding the site-wide restriction on non-authenticated users.
<location path="/admin">
<system.web>
<authorization>
<allow roles="Administrators" />
<deny users="*" />
</authorization>
</system.web>
</location>
The above configuration, when combined with the others, restricts access to the /admin folder and all resources
within it to members of the "Administrators" role. This restriction could also be applied by placing a separate
web.config file within the /admin folder root.
In addition to checking user role membership, you can also determine if they are authenticated (though often
this is better done using the location-based configuration covered above). Below is an example of this approach.
In the code above, role-based access control (RBAC) is used to determine whether certain elements of the page,
such as a SecretPanel , are visible based on the current user's role.
Typically, ASP.NET Web Forms applications configure security within the web.config file and then add additional
checks where needed in .aspx pages and their related .aspx.cs code-behind files. Most applications leverage
the universal membership provider, frequently with the additional role provider.
You can learn more about how to create custom policies in the documentation.
Whether you're using policies or roles, you can specify that a particular page in your Blazor application requires
that role or policy with the [Authorize] attribute, applied with the @attribute directive.
Requiring a role:
If you need access to a user's authentication state, roles, or claims in your code, there are two primary ways to
achieve this functionality. The first is to receive the authentication state as a cascading parameter. The second is
to access the state using an injected AuthenticationStateProvider . The details of each of these approaches are
described in the Blazor Security documentation.
The following code shows how to receive the AuthenticationState as a cascading parameter:
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
With this parameter in place, you can get the user using this code:
@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
With the provider in place, you can gain access to the user with the following code:
if (user.Identity.IsAuthenticated)
{
// work with user.Claims and/or user.Roles
}
Note: The AuthorizeView component, covered later in this chapter, provides a declarative way to control what a
user sees on a page or component.
To work with users and claims (in Blazor Server applications) you may also need to inject a UserManager<T> (use
IdentityUser for default) which you can use to enumerate and modify claims for a user. First inject the type and
assign it to a property:
Then use it to work with the user's claims. The following sample shows how to add and persist a claim on a user:
If you need to work with roles, follow the same approach. You may need to inject a RoleManager<T> (use
IdentityRole for default type) to list and manage the roles themselves.
Note: In Blazor WebAssembly projects, you will need to provide server APIs to perform these operations
(instead of using UserManager<T> or RoleManager<T> directly). A Blazor WebAssembly client application would
manage claims and/or roles by securely calling API endpoints exposed for this purpose.
Migration guide
Migrating from ASP.NET Web Forms and universal providers to ASP.NET Core Identity requires several steps:
1. Create ASP.NET Core Identity database schema in the destination database
2. Migrate data from universal provider schema to ASP.NET Core Identity schema
3. Migrate configuration from the web.config to middleware and services, typically in Startup.cs
4. Update individual pages using controls and conditionals to use tag helpers and new identity APIs.
Each of these steps is described in detail in the following sections.
Creating the ASP.NET Core Identity schema
There are several ways to create the necessary table structure used for ASP.NET Core Identity. The simplest is to
create a new ASP.NET Core Web application. Choose Web Application and then change Authentication to use
Individual User Accounts.
From the command line, you can do the same thing by running dotnet new webapp -au Individual . Once the app
has been created, run it and register on the site. You should trigger a page like the one shown below:
Click on the "Apply Migrations" button and the necessary database tables should be created for you. In addition,
the migration files should appear in your project, as shown:
You can run the migration yourself, without running the web application, using this command-line tool:
If you would rather run a script to apply the new schema to an existing database, you can script these migrations
from the command-line. Run this command to generate the script:
The above command will produce a SQL script in the output file auth.sql , which can then be run against
whatever database you like. If you have any trouble running dotnet ef commands, make sure you have the EF
Core tools installed on your system.
In the event you have additional columns on your source tables, you will need to identify the best location for
these columns in the new schema. Generally, columns found on the aspnet_Membership table should be mapped
to the AspNetUsers table. Columns on aspnet_Roles should be mapped to AspNetRoles . Any additional
columns on the aspnet_UsersInRoles table would be added to the AspNetUserRoles table.
It's also worth considering putting any additional columns on separate tables. So that future migrations won't
need to take into account such customizations of the default identity schema.
Migrating data from universal providers to ASP.NET Core Identity
Once you have the destination table schema in place, the next step is to migrate your user and role records to
the new schema. A complete list of the schema differences, including which columns map to which new
columns, can be found here.
To migrate your users from membership to the new identity tables, you should follow the steps described in the
documentation. After following these steps and the script provided, your users will need to change their
password the next time they log in.
It is possible to migrate user passwords but the process is much more involved. Requiring users to update their
passwords as part of the migration process, and encouraging them to use new, unique passwords, is likely to
enhance the overall security of the application.
Migrating security settings from web.config to Startup.cs
As noted above, ASP.NET membership and role providers are configured in the application's web.config file.
Since ASP.NET Core apps are not tied to IIS and use a separate system for configuration, these settings must be
configured elsewhere. For the most part, ASP.NET Core Identity is configured in the Startup.cs file. Open the
web project that was created earlier (to generate the identity table schema) and review its Startup.cs file.
The default ConfigureServices method adds support for EF Core and Identity:
// 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.AddRazorPages();
}
The AddDefaultIdentity extension method is used to configure Identity to use the default ApplicationDbContext
and the framework's IdentityUser type. If you're using a custom IdentityUser , be sure to specify its type here.
If these extension methods aren't working in your application, check that you have the appropriate using
statements and that you have the necessary NuGet package references. For example, your project should have
some version of the Microsoft.AspNetCore.Identity.EntityFrameworkCore and Microsoft.AspNetCore.Identity.UI
packages referenced.
Also in Startup.cs you should see the necessary middleware configured for the site. Specifically,
UseAuthentication and UseAuthorization should be set up, and in the proper location.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see
https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
ASP.NET Identity does not configure anonymous or role-based access to locations from Startup.cs . You will
need to migrate any location-specific authorization configuration data to filters in ASP.NET Core. Make note of
which folders and pages will require such updates. You will make these changes in the next section.
Updating individual pages to use ASP.NET Core Identity abstractions
In your ASP.NET Web Forms application, if you had web.config settings to deny access to certain pages or
folders to anonymous users, you would migrate these changes by adding the [Authorize] attribute to such
pages:
@attribute [Authorize]
If you further had denied access except to those users belonging to a certain role, you would likewise migrate
this behavior by adding an attribute specifying a role:
The [Authorize] attribute only works on @page components that are reached via the Blazor Router. The
attribute does not work with child components, which should instead use AuthorizeView .
If you have logic within page markup for determining whether to display some code to a certain user, you can
replace this with the AuthorizeView component. The AuthorizeView component selectively displays UI
depending on whether the user is authorized to see it. It also exposes a context variable that can be used to
access user information.
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you are authenticated.</p>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You are not signed in.</p>
</NotAuthorized>
</AuthorizeView>
You can access the authentication state within procedural logic by accessing the user from a
Task<AuthenticationState configured with the [CascadingParameter] attribute. This configuration will get you
access to the user, which can let you determine if they are authenticated and if they belong to a particular role. If
you need to evaluate a policy procedurally, you can inject an instance of the IAuthorizationService and calls the
AuthorizeAsync method on it. The following sample code demonstrates how to get user information and allow
an authorized user to perform a task restricted by the content-editor policy.
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
if (user.Identity.IsAuthenticated)
{
// Perform an action only available to authenticated (signed-in) users.
}
if (user.IsInRole("admin"))
{
// Perform an action only available to users in the 'admin' role.
}
The AuthenticationState first need to be set up as a cascading value before it can be bound to a cascading
parameter like this. That's typically done using the CascadingAuthenticationState component. This configuration
is typically done in App.razor :
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Summary
Blazor uses the same security model as ASP.NET Core, which is ASP.NET Core Identity. Migrating from universal
providers to ASP.NET Core Identity is relatively straightforward, assuming not too much customization was
applied to the original data schema. Once the data has been migrated, working with authentication and
authorization in Blazor apps is well documented, with configurable as well as programmatic support for most
security requirements.
References
Introduction to Identity on ASP.NET Core
Migrate from ASP.NET Membership authentication to ASP.NET Core 2.0 Identity
Migrate Authentication and Identity to ASP.NET Core
ASP.NET Core Blazor authentication and authorization
PR EVIO U S NE XT
Migrate from ASP.NET Web Forms to Blazor
3/6/2021 • 16 minutes to read • Edit Online
Migrating a code base from ASP.NET Web Forms to Blazor is a time-consuming task that requires planning. This
chapter outlines the process. Something that can ease the transition is to ensure the app adheres to an N-tier
architecture, wherein the app model (in this case, Web Forms) is separate from the business logic. This logical
separation of layers makes it clear what needs to move to .NET Core and Blazor.
For this example, the eShop app available on GitHub is used. eShop is a catalog service that provides CRUD
capabilities via form entry and validation.
Why should a working app be migrated to Blazor? Many times, there's no need. ASP.NET Web Forms will
continue to be supported for many years. However, many of the features that Blazor provides are only
supported on a migrated app. Such features include:
Performance improvements in the framework such as Span<T>
Ability to run as WebAssembly
Cross-platform support for Linux and macOS
App-local deployment or shared framework deployment without impacting other apps
If these or other new features are compelling enough, there may be value in migrating the app. The migration
can take different shapes; it can be the entire app, or only certain endpoints that require the changes. The
decision to migrate is ultimately based on the business problems to be solved by the developer.
The <packages> element includes all necessary dependencies. It's difficult to identify which of these packages
are included because you require them. Some <package> elements are listed simply to satisfy the needs of
dependencies you require.
The Blazor project lists the dependencies you require within an <ItemGroup> element in the project file:
<ItemGroup>
<PackageReference Include="Autofac" Version="4.9.3" />
<PackageReference Include="EntityFramework" Version="6.4.4" />
<PackageReference Include="log4net" Version="2.0.12" />
<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="2.2.12" />
</ItemGroup>
One NuGet package that simplifies the life of Web Forms developers is the Windows Compatibility Pack.
Although .NET is cross-platform, some features are only available on Windows. Windows-specific features are
made available by installing the compatibility pack. Examples of such features include the Registry, WMI, and
Directory Services. The package adds around 20,000 APIs and activates many services with which you may
already be familiar. The eShop project doesn't require the compatibility pack; but if your projects use Windows-
specific features, the package eases the migration efforts.
/// <summary>
/// Track the machine name and the start time for the session inside the current session
/// </summary>
protected void Session_Start(Object sender, EventArgs e)
{
HttpContext.Current.Session["MachineName"] = Environment.MachineName;
HttpContext.Current.Session["SessionStartTime"] = DateTime.Now;
}
/// <summary>
/// https://autofaccn.readthedocs.io/en/latest/integration/webforms.html
/// </summary>
private void ConfigureContainer()
{
var builder = new ContainerBuilder();
var mockData = bool.Parse(ConfigurationManager.AppSettings["UseMockData"]);
builder.RegisterModule(new ApplicationModule(mockData));
container = builder.Build();
_containerProvider = new ContainerProvider(container);
}
if (!mockData)
{
Database.SetInitializer<CatalogDBContext>(container.Resolve<CatalogDBInitializer>());
}
}
_log.Debug("Application_BeginRequest");
}
}
// 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.AddRazorPages();
services.AddServerSideBlazor();
if (Configuration.GetValue<bool>("UseMockData"))
{
services.AddSingleton<ICatalogService, CatalogServiceMock>();
}
else
{
services.AddScoped<ICatalogService, CatalogService>();
services.AddScoped<IDatabaseInitializer<CatalogDBContext>, CatalogDBInitializer>();
services.AddSingleton<CatalogItemHiLoGenerator>();
services.AddScoped(_ => new
CatalogDBContext(Configuration.GetConnectionString("CatalogDBContext")));
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddLog4Net("log4Net.xml");
if (Env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
ConfigDataBase(app);
}
private void ConfigDataBase(IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.CreateScope())
{
var initializer = scope.ServiceProvider.GetService<IDatabaseInitializer<CatalogDBContext>>();
if (initializer != null)
{
Database.SetInitializer(initializer);
}
}
}
}
One significant change you may notice from Web Forms is the prominence of DI. DI has been a guiding
principle in the ASP.NET Core design. It supports customization of almost all aspects of the ASP.NET Core
framework. There's even a built-in service provider that can be used for many scenarios. If more customization
is required, it can be supported by many community projects. For example, you can carry forward your third-
party DI library investment.
In the original eShop app, there's some configuration for session management. Since server-side Blazor uses
ASP.NET Core SignalR for communication, the session state isn't supported as the connections may occur
independent of an HTTP context. An app that uses the session state requires rearchitecting before running as a
Blazor app.
For more information about app startup, see App startup.
In the Enable startup process section, a lifecycle event was raised by Web Forms as the
Application_BeginRequest method. This event isn't available in ASP.NET Core. One way to achieve this behavior
is to implement middleware as seen in the Startup.cs file example. This middleware does the same logic and
then transfers control to the next handler in the middleware pipeline.
For more information on migrating modules and handlers, see Migrate HTTP handlers and modules to ASP.NET
Core middleware.
app.UseStaticFiles();
...
}
The eShop project enables basic static file access. There are many customizations available for static file access.
For information on enabling default files or a file browser, see Static files in ASP.NET Core.
<div class="container">
<div class="row">
<asp:Image runat="server" CssClass="col-md-6 esh-picture" ImageUrl='<%#"/Pics/" +
product.PictureFileName%>' />
<dl class="col-md-6 dl-horizontal">
<dt>Name
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.Name%>' />
</dd>
<dt>Description
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.Description%>' />
</dd>
<dt>Brand
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.CatalogBrand.Brand%>' />
</dd>
<dt>Type
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.CatalogType.Type%>' />
</dd>
<dt>Price
</dt>
<dd>
<asp:Label CssClass="esh-price" runat="server" Text='<%#product.Price%>' />
</dd>
<dt>Picture name
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.PictureFileName%>' />
</dd>
<dt>Stock
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.AvailableStock%>' />
</dd>
<dt>Restock
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.RestockThreshold%>' />
</dd>
<dt>Max stock
</dt>
<dd>
<asp:Label runat="server" Text='<%#product.MaxStockThreshold%>' />
</dd>
</dl>
</div>
</div>
</asp:Content>
namespace eShopLegacyWebForms.Catalog
{
public partial class Details : System.Web.UI.Page
{
private static readonly ILog _log =
LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
this.DataBind();
}
}
}
When converted to Blazor, the Web Forms page translates to the following code:
@page "/Catalog/Details/{id:int}"
@inject ICatalogService CatalogService
@inject ILogger<Details> Logger
<h2 class="esh-body-title">Details</h2>
<div class="container">
<div class="row">
<img class="col-md-6 esh-picture" src="@($"/Pics/{_item.PictureFileName}")">
<dd>
@_item.Name
</dd>
<dt>
Description
</dt>
<dd>
@_item.Description
</dd>
<dt>
Brand
</dt>
<dd>
@_item.CatalogBrand.Brand
</dd>
<dt>
Type
</dt>
<dd>
@_item.CatalogType.Type
</dd>
<dt>
Price
</dt>
<dd>
@_item.Price
</dd>
<dt>
Picture name
</dt>
<dd>
@_item.PictureFileName
</dd>
<dt>
Stock
</dt>
<dd>
@_item.AvailableStock
</dd>
<dt>
Restock
</dt>
<dd>
@_item.RestockThreshold
</dd>
<dt>
Max stock
</dt>
<dd>
@_item.MaxStockThreshold
</dd>
</dl>
</div>
</div>
@code {
private CatalogItem _item;
[Parameter]
public int Id { get; set; }
_item = CatalogService.FindCatalogItem(Id);
}
}
Notice that the code and markup are in the same file. Any required services are made accessible with the
@inject attribute. Per the @page directive, this page can be accessed at the Catalog/Details/{id} route. The
value of the route's {id} placeholder has been constrained to an integer. As described in the routing section,
unlike Web Forms, a Razor component explicitly states its route and any parameters that are included. Many
Web Forms controls may not have exact counterparts in Blazor. There's often an equivalent HTML snippet that
will serve the same purpose. For example, the <asp:Label /> control can be replaced with an HTML <label>
element.
Model validation in Blazor
If your Web Forms code includes validation, you can transfer much of what you have with little-to-no changes. A
benefit to running in Blazor is that the same validation logic can be run without needing custom JavaScript. Data
annotations enable easy model validation.
For example, the Create.aspx page has a data entry form with validation. An example snippet would look like
this:
<div class="form-group">
<label class="control-label col-md-2">Name</label>
<div class="col-md-3">
<asp:TextBox ID="Name" runat="server" CssClass="form-control"></asp:TextBox>
<asp:RequiredFieldValidator runat="server" ControlToValidate="Name" Display="Dynamic"
CssClass="field-validation-valid text-danger" ErrorMessage="The Name field is required." />
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Name</label>
<div class="col-md-3">
<InputText class="form-control" @bind-Value="_item.Name" />
<ValidationMessage For="(() => _item.Name)" />
</div>
</div>
...
</EditForm>
The EditForm context includes validation support and can be wrapped around an input. Data annotations are a
common way to add validation. Such validation support can be added via the DataAnnotationsValidator
component. For more information on this mechanism, see ASP.NET Core Blazor forms and validation.
Migrate configuration
In a Web Forms project, configuration data is most commonly stored in the web.config file. The configuration
data is accessed with ConfigurationManager . Services were often required to parse objects. With .NET
Framework 4.7.2, composability was added to the configuration via ConfigurationBuilders . These builders
allowed developers to add various sources for the configuration that was then composed at runtime to retrieve
the necessary values.
ASP.NET Core introduced a flexible configuration system that allows you to define the configuration source or
sources used by your app and deployment. The ConfigurationBuilder infrastructure that you may be using in
your Web Forms app was modeled after the concepts used in the ASP.NET Core configuration system.
The following snippet demonstrates how the Web Forms eShop project uses web.config to store configuration
values:
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
requirePermission="false" />
</configSections>
<connectionStrings>
<add name="CatalogDBContext" connectionString="Data Source=(localdb)\MSSQLLocalDB; Initial
Catalog=Microsoft.eShopOnContainers.Services.CatalogDb; Integrated Security=True;
MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="UseMockData" value="true" />
<add key="UseCustomizationData" value="false" />
</appSettings>
</configuration>
It's common for secrets, such as database connection strings, to be stored within the web.config. The secrets are
inevitably persisted in unsecure locations, such as source control. With Blazor on ASP.NET Core, the preceding
XML-based configuration is replaced with the following JSON:
{
"ConnectionStrings": {
"CatalogDBContext": "Data Source=(localdb)\\MSSQLLocalDB; Initial
Catalog=Microsoft.eShopOnContainers.Services.CatalogDb; Integrated Security=True;
MultipleActiveResultSets=True;"
},
"UseMockData": true,
"UseCustomizationData": false
}
JSON is the default configuration format; however, ASP.NET Core supports many other formats, including XML.
There are also several community-supported formats.
The constructor in the Blazor project's Startup class accepts an IConfiguration instance through a DI
technique known as constructor injection:
...
}
Architectural changes
Finally, there are some important architectural differences to consider when migrating to Blazor. Many of these
changes are applicable to anything based on .NET Core or ASP.NET Core.
Because Blazor is built on .NET Core, there are considerations in ensuring support on .NET Core. Some of the
major changes include the removal of the following features:
Multiple AppDomains
Remoting
Code Access Security (CAS)
Security Transparency
For more information on techniques to identify necessary changes to support running on .NET Core, see Port
your code from .NET Framework to .NET Core.
ASP.NET Core is a reimagined version of ASP.NET and has some changes that may not initially seem obvious. The
main changes are:
No synchronization context, which means there's no HttpContext.Current , Thread.CurrentPrincipal , or other
static accessors
No shadow copying
No request queue
Many operations in ASP.NET Core are asynchronous, which allows easier off-loading of I/O-bound tasks. It's
important to never block by using Task.Wait() or Task.GetResult() , which can quickly exhaust thread pool
resources.
Migration conclusion
At this point, you've seen many examples of what it takes to move a Web Forms project to Blazor. For a full
example, see the eShopOnBlazor project.
PR EVIO U S