Blazor For ASP NET Web Forms Developers
Blazor For ASP NET Web Forms Developers
ms/blazor-ebook
PUBLISHED BY
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.
All other marks and logos are property of their respective owners.
Authors:
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.
Additional resources
You can find the official Blazor home page and documentation at https://blazor.net.
Blazor ............................................................................................................................................................................................ 6
Configuration.......................................................................................................................................................................... 15
Pages.......................................................................................................................................................................................... 16
Layout ........................................................................................................................................................................................ 16
i Contents
Upgrading the BundleConfig Process .......................................................................................................................... 23
OnInitialized ....................................................................................................................................................................... 33
OnParametersSet ............................................................................................................................................................. 33
OnAfterRender .................................................................................................................................................................. 33
IDisposable ......................................................................................................................................................................... 34
Child content...................................................................................................................................................................... 35
Code-behind ........................................................................................................................................................................... 37
Navigation ............................................................................................................................................................................... 40
ii Contents
Forms and validation ............................................................................................................ 47
Additional resources ............................................................................................................................................................ 49
EF Code First....................................................................................................................................................................... 51
Katana ........................................................................................................................................................................................ 55
iii Contents
Updating individual pages to use ASP.NET Core Identity abstractions ..................................................... 73
Summary .................................................................................................................................................................................. 74
References ............................................................................................................................................................................... 75
iv Contents
CHAPTER 1
An introduction to Blazor
for ASP.NET Web Forms
developers
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:
Most modern web frameworks are now also open-source, which has a number of benefits. Users
aren’t beheld to a single project owner to fix bugs and add features. Open-source projects provide
improved transparency on development progress and upcoming changes. Open-source projects enjoy
contributions from an entire community, and they foster a supportive open-source ecosystem.
Despite the risks of open-source, many consumers and contributors have found suitable mitigations
that enable them to enjoy the benefits of an open-source ecosystem in a safe and reasonable way.
Examples of such mitigations include contributor license agreements, friendly licenses, pedigree scans,
and supporting foundations.
The .NET community has embraced both cross-platform support and open-source. .NET Core is an
open-source and cross-platform implementation of .NET that runs on a plethora of platforms,
including Windows, macOS, and various Linux distributions. Xamarin provides Mono, an open-source
version of .NET. Mono runs on Android, iOS, and a variety of other form factors, including watches
and smart TVs. In 2020, Microsoft released .NET 5 that reconciled .NET Core and Mono into “a single
.NET runtime and framework that can be used everywhere and that has uniform runtime behaviors
and developer experiences.”
Will ASP.NET Web Forms benefit from the move to open-source and cross-platform support? The
answer, unfortunately, is no, or at least not to the same extent as the rest of the platform. The .NET
team made it clear that ASP.NET Web Forms won’t be ported to .NET Core or .NET 6. Why is that?
There were efforts in the early days of .NET Core to port ASP.NET Web Forms. The number of breaking
changes required were found to be too drastic. There’s also an admission here that even for Microsoft,
there’s a limit to the number of web frameworks that it can support simultaneously. Perhaps someone
in the community will take up the cause of creating an open-source and cross-platform version of
ASP.NET Web Forms. The source code for ASP.NET Web Forms has been made available publicly in
reference form. But for the time being, it seems ASP.NET Web Forms will remain Windows-only and
without an open-source contribution model. If cross-platform support or open-source become
important for your scenarios, then you’ll need to look for something new.
Does this mean ASP.NET Web Forms is dead and should no longer be used? Of course not! As long as
the .NET Framework ships as part of Windows, ASP.NET Web Forms will be a supported framework.
For many Web Forms developers, the lack of cross-platform and open-source support is a non-issue.
If you don’t have a requirement for cross-platform support, open-source, or any of the other new
features in .NET Core or .NET 6, then sticking with ASP.NET Web Forms on Windows is fine. ASP.NET
Web Forms will continue to be a productive way to write web apps for many years to come.
But there’s another trend worth considering, and that’s the shift to the client.
However, browsers have become versatile platforms. They implement an ever-increasing number of
open web standards that grant access to the capabilities of the user’s machine. Why not take
advantage of the compute power, storage, memory, and other resources of the client device? UI
interactions in particular can benefit from a richer and more interactive feel when handled at least
partially or completely client-side. Logic and data that should be handled on the server can still be
handled server-side. Web API calls or even real-time protocols, like WebSockets, can be used. These
benefits are available to web developers for free if they’re willing to write JavaScript. Client-side UI
frameworks, such as Angular, React, and Vue, simplify client-side web development and have grown in
popularity. ASP.NET Web Forms developers can also benefit from leveraging the client, and even have
some out-of-the-box support with integrated JavaScript frameworks like ASP.NET AJAX.
But bridging two different platforms and ecosystems (.NET and JavaScript) comes with a cost.
Expertise is required in two parallel worlds with different languages, frameworks, and tools. Code and
logic can’t be easily shared between client and server, resulting in duplication and engineering
overhead. It can also be difficult to keep up with the JavaScript ecosystem, which has a history of
evolving at breakneck speed. Front-end framework and build tool preferences change quickly. The
industry has observed the progression from Grunt to Gulp to Webpack, and so on. The same restless
churn has occurred with front-end frameworks such as jQuery, Knockout, Angular, React, and Vue. But
given JavaScript’s browser monopoly, there was little choice in the matter. That is, until the web
community got together and caused a miracle to happen!
Work on running .NET on WebAssembly was announced in late 2017 and released in 2020, including
support in .NET 5 and beyond. The ability to run .NET code directly in the browser enables full-stack
web development with .NET.
Blazor has great tooling support in Visual Studio and Visual Studio Code. The framework also includes
a full UI component model and has built-in facilities for:
This book provides an introduction to Blazor that is catered specifically to ASP.NET Web Forms
developers. Each Blazor concept is presented in the context of analogous ASP.NET Web Forms
features and practices. By the end of this book, you’ll have an understanding of:
• HTML markup
• C# or Visual Basic code
• A code-behind class containing logic and event-handling capabilities
• Controls
Controls are reusable units of web UI that can be programmatically placed and interacted with on a
page. Pages are composed of files that end with .aspx containing markup, controls, and some code.
The code-behind classes are in files with the same base name and an .aspx.cs or .aspx.vb extension,
depending on the programming language used. Interestingly, the web server interprets contents of
the .aspx files and compiles them whenever they change. This recompilation occurs even if the web
server is already running.
Controls can be built with markup and delivered as user controls. A user control derives from the
UserControl class and has a similar structure to the Page. Markup for user controls is stored in an
.ascx file. An accompanying code-behind class resides in an .ascx.cs or .ascx.vb file. Controls can also
be built completely with code, by inheriting from either the WebControl or CompositeControl base
class.
Controls on a Page typically post-back to the same page that presented the control, and carry along
with them a payload from a hidden form field called ViewState. The ViewState field contains
information about the state of the controls at the time they were rendered and presented on the
page, allowing the ASP.NET runtime to compare and identify changes in the content submitted to the
server.
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).
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.
The downloaded assemblies are normal .NET assemblies, like you would use in any other .NET app.
Because the runtime supports .NET Standard, you can use existing .NET Standard libraries with your
Blazor WebAssembly app. However, these assemblies will still execute in the browser security
sandbox. Some functionality may throw a PlatformNotSupportedException, like trying to access the
file system or opening arbitrary network connections.
When the app loads, the .NET runtime is started and pointed at the app assembly. The app startup
logic runs, and the root components are rendered. Blazor calculates the UI updates based on the
rendered output from the components. The DOM updates are then applied.
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.
In Blazor Server apps, the components run on the server instead of client-side in the browser. UI
events that occur in the browser are sent to the server over a real-time connection. The events are
dispatched to the correct component instances. The components render, and the calculated UI diff is
serialized and sent to the browser where it’s applied to the DOM.
• There’s no .NET server-side dependency. The app is fully functioning after downloaded to the
client.
• Client resources and capabilities are fully leveraged.
• Work is offloaded from the server to the client.
• An ASP.NET Core web server isn’t required to host the app. Serverless deployment scenarios are
possible (for example, serving the app from a CDN).
The downsides of the Blazor WebAssembly hosting model are:
• Download size is much smaller than a client-side app, and the app loads much faster.
• The app takes full advantage of server capabilities, including use of any .NET compatible APIs.
• .NET on the server is used to run the app, so existing .NET tooling, such as debugging, works as
expected.
• Thin clients are supported. For example, server-side apps work with browsers that don’t support
WebAssembly and on resource-constrained devices.
• The app’s .NET/C# code base, including the app’s component code, isn’t served to clients.
The downsides to the Blazor Server hosting model are:
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>net6.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>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore"
Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore"
Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
</Project>
By comparison, a default ASP.NET Web Forms project includes almost 300 lines of XML in its .csproj
file, most of which is explicitly listing the various code and content files in the project. Since the
release of .NET 5 both Blazor Server and Blazor WebAssembly app can easily share one unified
runtime.
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="13.0.1" />
</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.
using BlazorApp3.Areas.Identity;
using BlazorApp3.Data;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
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 don’t use a Global.asax file to define the startup logic for the app. Instead, this logic is
contained in Program.cs or in a related Startup class that is referenced from Program.cs. Either way,
this code is used to configure the app and any app-specific services.
In a Blazor Server app, the Program.cs file shown is used to set up the endpoint for the real-time
connection used by Blazor between the client browsers and the server.
In a Blazor WebAssembly app, the Program.cs file defines the root components for the app and where
they should be rendered:
using BlazorApp1;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
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 as 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 exposure 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:
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 or ASP.NET Core Razor 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 render-mode attribute 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.
Option Description
RenderMode.Server Rendered interactively once a connection with the browser is
established
RenderMode.ServerPrerendered First prerendered and then rendered interactively
RenderMode.Static Rendered as static content
The *_Layout.cshtml* file includes the default HTML for the app and its components.
@using Microsoft.AspNetCore.Components.Web
@namespace BlazorApp3.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" />
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="BlazorApp3.styles.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@RenderBody()
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<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 lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-
scale=1.0, user-scalable=no" />
<title>BlazorApp1</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="BlazorApp1.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.cs file 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.
Both Blazor Server and Blazor WebAssembly apps support Hot Reload in Visual Studio. Hot Reload is
a feature that automatically updates changes made to a Blazor app live, in the browser. You can
toggle whether Hot Reload is enabled from its icon in the toolbar:
Selecting the caret beside the icon reveals additional options. You can toggle Hot Reload on or off,
restart the application, and toggle whether Hot Reload should occur whenever a file is saved.
You can also access additional configuration options. The configuration dialog lets you specify
whether Hot Reload should be enabled when debugging (along with Edit and Continue), when
starting without debugging, or when a file is saved.
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.
In Program.cs, you configure the app’s required services by adding them to the built-in services
container and then set up the app’s HTTP request pipeline.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
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.
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 app.Map* statements define 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.
The Grunt, Gulp, and WebPack command-line tools and their associated configurations can be added
to your application and ASP.NET Core will quietly ignore those files during the application build
process. You can add a call to run their tasks by adding a Target inside your project file with syntax
similar to the following that would trigger a gulp script and the min target inside that script:
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.
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:
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.
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.
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:
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.
@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.
@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; }
}
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:
To change the event used for the bind, use the @bind:event attribute.
@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.
@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.
@code {
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged
}
}
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.
OnParametersSet
The OnParametersSet and OnParametersSetAsync methods are called when a component has
received parameters from its parent and the value are assigned to properties. These methods are
executed after component initialization and each time the component is rendered.
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.
The firstRender parameter is true the first time the component is rendered; otherwise, its value is
false.
IDisposable
Blazor components can implement IDisposable to dispose of resources when the component is
removed from the UI. A Razor component can implement IDispose by using the @implements
directive:
@using System
@implements IDisposable
...
@code {
public void Dispose()
{
...
}
}
To capture a component reference in Blazor, use the @ref directive attribute. The value of the
attribute should match the name of a settable field with the same type as the referenced component.
@code {
MyLoginDialog loginDialog;
void OnSomething()
{
loginDialog.Show();
}
}
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.
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>
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:
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.
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 forwards 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
Name.razor
@page "/Name"
@layout MainLayout
<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:
<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.
@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>
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.
@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>
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.
With Blazor Server, the app maintains an ongoing connection with the server. The app’s state, called a
circuit, is held in server memory while the connection is considered active. State will only be disposed
when the user navigates away from the app or a particular page in the app. All members of the active
components are available between interactions with the server.
• In a shopping cart application, persist the content of new items added to the cart in a database
record. If the state on the server is lost, you can reconstitute it from the database records.
The signature of the .NET Framework Session object isn’t the same as the ASP.NET Core Session
object. Consider the documentation for the new ASP.NET Core Session before deciding to migrate
and use the new session state feature.
Session is available in ASP.NET Core and Blazor Server, but is discouraged from use in favor of storing
data in a data repository appropriately. Session state is also not functional if visitors decline the use
HTTP cookies in your application due to privacy concerns.
Configuration for ASP.NET Core and Session state is available in the Session and state management in
ASP.NET Core article.
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:
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.
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.
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:
[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:
<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>
<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.
• 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
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.
[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:
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:
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.
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.
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.
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:
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.
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:
{
"section0": {
"key0": "value",
"key1": "value"
},
"section1": {
"key0": "value",
"key1": "value"
}
}
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.
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:
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.
In ASP.NET Core, you can specify a class hierarchy that will receive the configuration values. These
classes:
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.
The Universal Providers work with a SQL database schema that includes tables like
aspnet_Applications, aspnet_Membership, aspnet_Roles, and aspnet_Users. When configured by
running the aspnet_regsql.exe command, the providers install tables and stored procedures that
provide all of the necessary queries and commands to work with the underlying data. The database
schema and these stored procedures are not compatible with newer ASP.NET Identity and ASP.NET
Core Identity systems, so existing data must be migrated into the new system. Figure 1 shows an
example table schema configured for universal providers.
63 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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.
<?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 authentication configuration 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.
64 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
<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.
This code can be used both in code-behind logic as well as in the page itself:
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.
65 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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.
In addition to roles, ASP.NET Core identity supports the concepts of claims and policies. While a role
should specifically correspond to a set of resources a user in that role should be able to access, a
claim is simply part of a user’s identity. A claim is a name value pair that represents what the subject
is, not what the subject can do.
It is possible to directly inspect a user’s claims and determine based on these values whether a user
should be given access to a resource. However, such checks are often repetitive and scattered
throughout the system. A better approach is to define a policy.
An authorization policy consists of one or more requirements. Policies are registered as part of the
authorization service configuration in the ConfigureServices method of Startup.cs. For example,
the following code snippet configures a policy called “CanadiansOnly”, which has the requirement that
the user has the Country claim with the value of “Canada”.
services.AddAuthorization(options =>
{
options.AddPolicy("CanadiansOnly", policy => policy.RequireClaim(ClaimTypes.Country,
"Canada"));
});
You can learn more about how to create custom policies in the documentation.
66 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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:
67 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
private async Task AddCountryClaim()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var identityUser = await MyUserManager.FindByNameAsync(user.Identity.Name);
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:
68 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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:
69 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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:
70 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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.
71 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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.
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 Program.cs you should see the necessary middleware configured for the site. Specifically,
UseAuthentication and UseAuthorization should be set up, and in the proper location.
app.UseHttpsRedirection();
app.UseStaticFiles();
72 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
//app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
ASP.NET Identity does not configure anonymous or role-based access to locations from Program.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.
@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.
73 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
@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
74 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
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
75 CHAPTER 13 | Security: Authentication and Authorization in ASP.NET Web Forms and Blazor
CHAPTER 14
Migrate from ASP.NET
Web Forms to Blazor
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:
• Running as WebAssembly doesn’t support all features (such as threading) at the current time
• Chatty communication between the client and server may cause latency issues in server-side
mode
Once the project has been created, install the libraries that were used in the previous project. In older
Web Forms projects, you may have used the packages.config file to list the required NuGet packages.
In the new SDK-style project, packages.config has been replaced with <PackageReference> elements
in the project file. A benefit to this approach is that all dependencies are installed transitively. You only
list the top-level dependencies you care about.
Many of the dependencies you’re using are available for .NET, including Entity Framework 6 and
log4net. If there’s no .NET or .NET Standard version available, the .NET Framework version can often
be used. Your mileage may vary. Any API used that isn’t available in .NET causes a runtime error. Visual
Studio notifies you of such packages. A yellow icon appears on the project’s References node in
Solution Explorer.
In the Blazor-based eShop project, you can see the packages that are installed. Previously, the
packages.config file listed every package used in the project, resulting in a file almost 50 lines long. A
snippet of packages.config is:
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.
The Global.asax.cs file is the default startup page for Web Forms projects. In the eShop project, this file
configures the Inversion of Control (IoC) container and handles the various lifecycle events of the app
or request. Some of these events are handled with middleware (such as Application_BeginRequest).
Other events require the overriding of specific services via dependency injection (DI).
By way of example, the Global.asax.cs file for eShop, contains the following code:
/// <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");
}
}
using eShopOnBlazor.Models;
using eShopOnBlazor.Models.Infrastructure;
using eShopOnBlazor.Services;
using log4net;
using System.Data.Entity;
using eShopOnBlazor;
// add services
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
if (builder.Configuration.GetValue<bool>("UseMockData"))
{
builder.Services.AddSingleton<ICatalogService, CatalogServiceMock>();
}
else
{
builder.Services.AddScoped<ICatalogService, CatalogService>();
builder.Services.AddScoped<IDatabaseInitializer<CatalogDBContext>,
CatalogDBInitializer>();
builder.Services.AddSingleton<CatalogItemHiLoGenerator>();
builder.Services.AddScoped(_ => new
CatalogDBContext(builder.Configuration.GetConnectionString("CatalogDBContext")));
}
new LoggerFactory().AddLog4Net("log4Net.xml");
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
ConfigDataBase(app);
if (initializer != null)
{
Database.SetInitializer(initializer);
}
}
}
app.Run();
One significant change you may notice from Web Forms is the prominence of dependency injection
(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.
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.
...
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.
For more information on bundling and minification, see Bundle and minify static assets in ASP.NET
Core.
For example, the details view comprises three files in the Web Forms project: Details.aspx,
Details.aspx.cs, and Details.aspx.designer.cs. When converting to Blazor, the code-behind and markup
are combined into Details.razor. Razor compilation (equivalent to what’s in .designer.cs files) is stored
in the obj directory and isn’t, by default, viewable in Solution Explorer. The Web Forms page consists
of the following markup:
<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>
<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>
using eShopLegacyWebForms.Models;
using eShopLegacyWebForms.Services;
using log4net;
using System;
using System.Web.UI;
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>
<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
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 run time to retrieve the necessary values.
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.
if (builder.Configuration.GetValue<bool>("UseMockData"))
{
builder.Services.AddSingleton<ICatalogService, CatalogServiceMock>();
}
else
{
builder.Services.AddScoped<ICatalogService, CatalogService>();
builder.Services.AddScoped<IDatabaseInitializer<CatalogDBContext>,
CatalogDBInitializer>();
builder.Services.AddSingleton<CatalogItemHiLoGenerator>();
builder.Services.AddScoped(_ => new
CatalogDBContext(builder.Configuration.GetConnectionString("CatalogDBContext")));
}
• In .NET Framework, the DbContext object accepts a string of the form name=ConnectionString
and uses the connection string from ConfigurationManager.AppSettings[ConnectionString]
to connect. In .NET Core, this isn’t supported. The connection string must be supplied.
• The database was accessed in a synchronous way. Though this works, scalability may suffer. This
logic should be moved to an asynchronous pattern.
Although there isn’t the same native support for dataset binding, Blazor provides flexibility and power
with its C# support in a Razor page. For example, you can perform calculations and display the result.
For more information on data patterns in Blazor, see the Data access chapter.
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:
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.