Freigeben über


Migrieren von ASP.NET Web Forms zu Blazor

Tipp

Diese Inhalte sind ein Auszug aus dem eBook „Blazor for ASP NET Web Forms Developers for Azure“, verfügbar unter .NET Docs oder als kostenlos herunterladbare PDF-Datei, die offline gelesen werden kann.

Blazor-for-ASP-NET-Web-Forms-Developers eBook cover thumbnail.

Das Migrieren einer Codebasis von ASP.NET Web Forms zu Blazor ist eine zeitaufwendige Aufgabe, die sorgfältiger Planung bedarf. Im vorliegenden Kapitel wird der Prozess beschrieben. Sie können den Übergang erleichtern, indem Sie sicherstellen, dass die App eine n-schichtige Architektur verwendet, in der das App-Modell (in diesem Fall Web Forms) von der Geschäftslogik getrennt ist. Diese logische Trennung der Schichten macht klar, welche Elemente nach .NET Core und Blazor verschoben werden müssen.

In diesem Beispiel wird die eShop-App verwendet, die auf GitHub zur Verfügung steht. eShop ist ein Katalogdienst, der CRUD-Funktionen per Formulareingabe und -überprüfung bereitstellt.

Warum sollte eine funktionierende App zu Blazor migriert werden? In vielen Fällen besteht kein Bedarf daran. ASP.NET Web Forms wird noch viele Jahre lang unterstützt. Allerdings werden viele der Features, die Blazor bietet, nur in einer migrierten App unterstützt. Dazu gehören folgende Features:

  • Leistungsverbesserungen im Framework, z. B. Span<T>
  • Möglichkeit zur Ausführung als WebAssembly
  • Plattformübergreifende Unterstützung für Linux und macOS
  • Lokale App-Bereitstellung oder Bereitstellung eines gemeinsam genutzten Frameworks ohne Beeinträchtigung anderer Apps

Wenn diese oder andere neue Features überzeugend genug sind, kann die Migration der App einen Mehrwert bieten. Die Migration kann verschiedene Formen annehmen: Möglicherweise geht es um die gesamte App, vielleicht sind auch nur für bestimmte Endpunkte Änderungen erforderlich. Die Entscheidung über eine Migration basiert letztendlich auf den geschäftlichen Problemen, die vom Entwickler gelöst werden sollen.

Serverseitiges und clientseitiges Hosting im Vergleich

Wie im Kapitel Hostingmodelle beschrieben, kann eine Blazor-App auf zwei verschiedene Arten gehostet werden: auf Serverseite oder auf Clientseite. Das serverseitige Modell verwendet ASP.NET Core-SignalR-Verbindungen, um die DOM-Aktualisierungen zu verwalten, und führt gleichzeitig tatsächlichen Code auf dem Server aus. Das clientseitige Modell wird als WebAssembly in einem Browser ausgeführt und erfordert keine Serververbindungen. Es gibt einige Unterschiede, die beeinflussen können, was sich für eine bestimmte App am besten eignet:

  • Die Ausführung als WebAssembly unterstützt zurzeit nicht alle Features (z. B. Threading).
  • Ein hohes Kommunikationsaufkommen zwischen Client und Server kann im serverseitigen Modus zu Latenzproblemen führen.
  • Der Zugriff auf Datenbanken und interne oder geschützte Dienste erfordert beim clientseitigen Hosting einen separaten Dienst.

Zu dem Zeitpunkt, als das vorliegende Kapitel geschrieben wurde, ähnelte das serverseitige Modell eher Web Forms. Dieses Kapitel beschäftigt sich primär mit dem Modell des serverseitigen Hostings, da dieses bereits produktionsreif ist.

Erstellen eines neuen Projekts

Der erste Schritt bei Migration ist die Erstellung eines neuen Projekts. Der Projekttyp basiert auf den .NET-Projekten im SDK-Stil und vereinfacht einen Großteil der Vorlage, die in früheren Projektformaten verwendet wurde. Genauere Details finden Sie im Kapitel zur Projektstruktur.

Wenn das Projekt erstellt ist, installieren Sie die Bibliotheken, die im vorherigen Projekt verwendet wurden. In älteren Web Forms-Projekten haben Sie möglicherweise die Datei packages.config verwendet, um die erforderlichen NuGet-Pakte aufzulisten. Im neuen Projekttyp im SDK-Stil wurde packages.config durch <PackageReference>-Elemente in der Projektdatei ersetzt. Dieser Ansatz bietet u. a. den Vorteil, dass alle Abhängigkeiten transitiv installiert werden. Sie listen nur die Abhängigkeiten auf oberster Ebene auf, die Sie benötigen.

Viele der von Ihnen verwendeten Abhängigkeiten, einschließlich Entity Framework 6 und log4net, sind für .NET verfügbar. Wenn keine .NET- oder .NET Standard-Version verfügbar ist, kann häufig auch die .NET Framework-Version verwendet werden. Ihre Erfahrungen können hiervon abweichen. Jede verwendete API, die in .NET nicht verfügbar ist, verursacht einen Laufzeitfehler. Visual Studio benachrichtigt Sie bei solchen Paketen. Im Projektmappen-Explorer wird im Knoten Verweise ein gelbes Symbol angezeigt.

Im Blazor-basierten eShop-Projekt sehen Sie alle installierten Pakete. Zuvor wurden in der Datei packages.config sämtliche Pakete aufgelistet, die im Projekt verwendet wurden. Damit war diese Datei fast 50 Zeilen lang. Hier sehen Sie einen Codeausschnitt von packages.config:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  ...
  <package id="Microsoft.ApplicationInsights.Agent.Intercept" version="2.4.0" targetFramework="net472" />
  <package id="Microsoft.ApplicationInsights.DependencyCollector" version="2.9.1" targetFramework="net472" />
  <package id="Microsoft.ApplicationInsights.PerfCounterCollector" version="2.9.1" targetFramework="net472" />
  <package id="Microsoft.ApplicationInsights.Web" version="2.9.1" targetFramework="net472" />
  <package id="Microsoft.ApplicationInsights.WindowsServer" version="2.9.1" targetFramework="net472" />
  <package id="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel" version="2.9.1" targetFramework="net472" />
  <package id="Microsoft.AspNet.FriendlyUrls" version="1.0.2" targetFramework="net472" />
  <package id="Microsoft.AspNet.FriendlyUrls.Core" version="1.0.2" targetFramework="net472" />
  <package id="Microsoft.AspNet.ScriptManager.MSAjax" version="5.0.0" targetFramework="net472" />
  <package id="Microsoft.AspNet.ScriptManager.WebForms" version="5.0.0" targetFramework="net472" />
  ...
  <package id="System.Memory" version="4.5.1" targetFramework="net472" />
  <package id="System.Numerics.Vectors" version="4.4.0" targetFramework="net472" />
  <package id="System.Runtime.CompilerServices.Unsafe" version="4.5.0" targetFramework="net472" />
  <package id="System.Threading.Channels" version="4.5.0" targetFramework="net472" />
  <package id="System.Threading.Tasks.Extensions" version="4.5.1" targetFramework="net472" />
  <package id="WebGrease" version="1.6.0" targetFramework="net472" />
</packages>

Das <packages>-Element enthält alle erforderlichen Abhängigkeiten. Es ist schwierig herauszufinden, welche dieser Pakete eingeschlossen wurden, weil sie tatsächlich benötigt wurden. Einige <package>-Elemente werden nur aufgelistet, um die Anforderungen der von Ihnen benötigten Abhängigkeiten zu erfüllen.

Das Blazor-Projekt listet alle erforderlichen Abhängigkeiten in einem <ItemGroup>-Element in der Projektdatei auf:

<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>

Ein NuGet-Paket, das Web Forms-Entwicklern das Leben erleichtert, ist das Windows Compatibility Pack. Obwohl .NET plattformübergreifend funktioniert, sind einige Features nur unter Windows verfügbar. Windows-spezifische Features werden durch Installation des Compatibility Pack zur Verfügung gestellt. Die Registrierung, WMI und die Verzeichnisdienste sind nur einige Beispiele für solche Features. Das Paket fügt rund 20.000 APIs hinzu und aktiviert viele Dienste, mit denen Sie möglicherweise bereits vertraut sind. Für das eShop-Projekt ist das Compatibility Pack nicht erforderlich, aber wenn Ihre Projekte Windows-spezifische Features nutzen, vereinfacht das Paket die Migration.

Aktivieren des Startprozesses

Der Startprozess für Blazor unterscheidet sich von dem für Web Forms und folgt einem ähnlichen Setupablauf wie bei anderen ASP.NET Core-Diensten. Beim serverseitigen Hosting werden Razor-Komponenten als Teil einer normalen ASP.NET Core-App ausgeführt. Beim Hosten im Browser mit WebAssembly verwenden Razor-Komponenten ein ähnliches Hostingmodell. Der Unterschied besteht darin, dass die Komponenten als von allen Back-End-Prozessen getrennter Dienst ausgeführt werden. Bei beiden Varianten läuft der Start gleich ab.

Die Datei Global.asax.cs ist die standardmäßige Startseite für Web Forms-Projekte. Im eShop-Projekt konfiguriert diese Datei den IoC-Container (Inversion of Control, Steuerungsumkehr) und verarbeitet die verschiedenen Lebenszyklusereignisse der App oder Anforderung. Einige dieser Ereignisse werden per Middleware verarbeitet (z. B. Application_BeginRequest). Andere Ereignisse erfordern das Überschreiben bestimmter Dienste per Abhängigkeitsinjektion (Dependency Injection, DI).

Die Datei Global.asax.cs für eShop enthält beispielsweise den folgenden Code:

public class Global : HttpApplication, IContainerProviderAccessor
{
    private static readonly ILog _log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    static IContainerProvider _containerProvider;
    IContainer container;

    public IContainerProvider ContainerProvider
    {
        get { return _containerProvider; }
    }

    protected void Application_Start(object sender, EventArgs e)
    {
        // Code that runs on app startup
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        ConfigureContainer();
        ConfigDataBase();
    }

    /// <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);
    }

    private void ConfigDataBase()
    {
        var mockData = bool.Parse(ConfigurationManager.AppSettings["UseMockData"]);

        if (!mockData)
        {
            Database.SetInitializer<CatalogDBContext>(container.Resolve<CatalogDBInitializer>());
        }
    }

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        //set the property to our new object
        LogicalThreadContext.Properties["activityid"] = new ActivityIdHelper();

        LogicalThreadContext.Properties["requestinfo"] = new WebRequestInfo();

        _log.Debug("Application_BeginRequest");
    }
}

Die vorherige Datei wird zur Program.cs-Datei auf der Serverseite Blazor:

using eShopOnBlazor.Models;
using eShopOnBlazor.Models.Infrastructure;
using eShopOnBlazor.Services;
using log4net;
using System.Data.Entity;
using eShopOnBlazor;

var builder = WebApplication.CreateBuilder(args);

// 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")));
}

var app = builder.Build();

new LoggerFactory().AddLog4Net("log4Net.xml");

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
}

// Middleware for Application_BeginRequest
app.Use((ctx, next) =>
{
    LogicalThreadContext.Properties["activityid"] = new ActivityIdHelper(ctx);
    LogicalThreadContext.Properties["requestinfo"] = new WebRequestInfo(ctx);
    return next();
});

app.UseStaticFiles();

app.UseRouting();

app.UseEndpoints(endpoints =>
{
    endpoints.MapBlazorHub();
    endpoints.MapFallbackToPage("/_Host");
});

ConfigDataBase(app);

void ConfigDataBase(IApplicationBuilder app)
{
    using (var scope = app.ApplicationServices.CreateScope())
    {
        var initializer = scope.ServiceProvider.GetService<IDatabaseInitializer<CatalogDBContext>>();

        if (initializer != null)
        {
            Database.SetInitializer(initializer);
        }
    }
}

app.Run();

Eine wesentliche Veränderung gegenüber Web Forms ist die Bedeutung der Abhängigkeitsinjektion (DI). Die Abhängigkeitsinjektion ist seit jeher ein Leitprinzip im ASP.NET Core-Design. Sie unterstützt die Anpassung nahezu aller Aspekte des ASP.NET Core-Frameworks. Es gibt sogar einen integrierten Dienstanbieter, der für viele Szenarien verwendet werden kann. Wenn ein höheres Maß an Anpassung erforderlich ist, kann er von vielen Communityprojekten unterstützt werden. Sie können beispielsweise Ihre Investitionen in DI-Bibliotheken von Drittanbietern weiter nutzen.

Die ursprüngliche eShop-App umfasst Konfigurationscode für die Sitzungsverwaltung. Da serverseitiges Blazor ASP.NET Core SignalR für die Kommunikation verwendet, wird der Sitzungszustand nicht unterstützt, da die Verbindungen möglicherweise unabhängig von einem HTTP-Kontext hergestellt werden. Für eine App, die den Sitzungszustand verwendet, sind einige Änderungen an der Architektur erforderlich, bevor sie als Blazor-App ausgeführt werden kann.

Weitere Informationen zum App-Start finden Sie unter App-Start.

Migrieren von HTTP-Modulen und Handlern zu Middleware

HTTP-Module und Handler sind gängige Muster in Web Forms zum Steuern der HTTP-Anforderungspipeline. Klassen, die IHttpModule oder IHttpHandler implementieren, können registriert werden und eingehende Anforderungen verarbeiten. Web Forms konfiguriert Module und Handler in der Datei web.config. Web Forms basiert auch in wesentlichen Teilen auf der Verarbeitung von App-Lebenszyklusereignissen. ASP.NET Core verwendet stattdessen Middleware. Middleware wird in der Configure-Methode der Startup-Klasse registriert. Die Ausführungsreihenfolge von Middleware wird durch die Reihenfolge der Registrierung bestimmt.

Im Abschnitt Aktivieren des Startprozesses wurde ein Lebenszyklusereignis von Web Forms als Application_BeginRequest-Methode ausgelöst. Dieses Ereignis ist in ASP.NET Core nicht verfügbar. Eine Möglichkeit, dieses Verhalten zu erreichen, ist die Implementierung von Middleware, wie im Beispiel der Datei Startup.cs veranschaulicht. Diese Middleware führt dieselbe Logik aus und übergibt die Steuerung dann an den nächsten Handler in der Middlewarepipeline.

Weitere Informationen zum Migrieren von Modulen und Handlern finden Sie unter Migrieren von HTTP-Handlern und Modulen zu ASP.NET Core Middleware.

Migrieren von statischen Dateien

Damit statische Dateien (z. B. HTML, CSS, Bilder und JavaScript) bereitgestellt werden können, müssen diese von der Middleware verfügbar gemacht werden. Der Aufruf der UseStaticFiles-Methode ermöglicht die Bereitstellung statischer Dateien aus dem Webstammpfad. Das standardmäßige Webstammverzeichnis lautet wwwroot, kann aber angepasst werden. Wie in der Program.cs-Datei angegeben:

...

app.UseStaticFiles();

...

Das eShop-Projekt ermöglicht grundlegenden Zugriff auf statische Dateien. Für den Zugriff auf statische Dateien sind viele Anpassungen verfügbar. Informationen zum Aktivieren von Standarddateien oder eines Dateibrowsers finden Sie unter Statische Dateien in ASP.NET Core.

Migrieren des Setups für Bündelung und Minimierung zur Laufzeit

Bündelung und Minimierung sind Techniken zur Leistungsoptimierung, mit denen die Anzahl und die Größe von Serveranforderungen für den Abruf bestimmter Dateitypen reduziert werden. Bei JavaScript und CSS erfolgt häufig eine gewisse Form der Bündelung oder Minimierung, bevor Dateien an den Client gesendet werden. In ASP.NET Web Forms werden diese Optimierungen zur Laufzeit verarbeitet. Die Optimierungskonventionen sind in der Datei App_Start/BundleConfig.cs definiert. In ASP.NET Core wird ein eher deklarativer Ansatz gewählt. Eine Datei listet die zu minimierenden Dateien sowie bestimmte Minimierungseinstellungen auf.

Weitere Informationen zu Bündelung und Minimierung finden Sie unter Bündelung und Minimierung statischer Ressourcen in ASP.NET Core.

Migrieren von ASPX-Seiten

Eine Seite in eine Web Forms-App ist eine Datei mit der Erweiterung .aspx. Eine Web Forms-Seite kann häufig einer Komponente in Blazor zugeordnet werden. Eine Razor-Komponente wird in einer Datei mit der Erweiterung .razor erstellt. Für das eShop-Projekt werden fünf Seiten in eine Razor-Seite konvertiert.

Die Detailansicht besteht beispielsweise aus drei Dateien im Web Forms-Projekt: Details.aspx, Details.aspx.cs und Details.aspx.designer.cs. Beim Konvertieren zu Blazor werden CodeBehind und Markup in Details.razor kombiniert. Die Razor-Zusammenstellung (äquivalent zum Inhalt der .designer.cs-Dateien) wird im Verzeichnis obj gespeichert und ist standardmäßig nicht im Projektmappen-Explorer sichtbar. Die Web Forms-Seite besteht aus folgendem Markup:

<%@ Page Title="Details" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Details.aspx.cs" Inherits="eShopLegacyWebForms.Catalog.Details" %>

<asp:Content ID="Details" ContentPlaceHolderID="MainContent" runat="server">
    <h2 class="esh-body-title">Details</h2>

    <div class="container">
        <div class="row">
            <asp:Image runat="server" CssClass="col-md-6 esh-picture" ImageUrl='<%#"/Pics/" + product.PictureFileName%>' />
            <dl class="col-md-6 dl-horizontal">
                <dt>Name
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.Name%>' />
                </dd>

                <dt>Description
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.Description%>' />
                </dd>

                <dt>Brand
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.CatalogBrand.Brand%>' />
                </dd>

                <dt>Type
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.CatalogType.Type%>' />
                </dd>
                <dt>Price
                </dt>

                <dd>
                    <asp:Label CssClass="esh-price" runat="server" Text='<%#product.Price%>' />
                </dd>

                <dt>Picture name
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.PictureFileName%>' />
                </dd>

                <dt>Stock
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.AvailableStock%>' />
                </dd>

                <dt>Restock
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.RestockThreshold%>' />
                </dd>

                <dt>Max stock
                </dt>

                <dd>
                    <asp:Label runat="server" Text='<%#product.MaxStockThreshold%>' />
                </dd>

            </dl>
        </div>

        <div class="form-actions no-color esh-link-list">
            <a runat="server" href='<%# GetRouteUrl("EditProductRoute", new {id =product.Id}) %>' class="esh-link-item">Edit
            </a>
            |
            <a runat="server" href="~" class="esh-link-item">Back to list
            </a>
        </div>

    </div>
</asp:Content>

Das vorherige Markup-CodeBehind enthält folgenden Code:

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);

        protected CatalogItem product;

        public ICatalogService CatalogService { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            var productId = Convert.ToInt32(Page.RouteData.Values["id"]);
            _log.Info($"Now loading... /Catalog/Details.aspx?id={productId}");
            product = CatalogService.FindCatalogItem(productId);

            this.DataBind();
        }
    }
}

Bei der Konvertierung zu Blazor wird die Web Forms-Seite in folgenden Code übersetzt:

@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}")">

        <dl class="col-md-6 dl-horizontal">
            <dt>
                Name
            </dt>

            <dd>
                @_item.Name
            </dd>

            <dt>
                Description
            </dt>

            <dd>
                @_item.Description
            </dd>

            <dt>
                Brand
            </dt>

            <dd>
                @_item.CatalogBrand.Brand
            </dd>

            <dt>
                Type
            </dt>

            <dd>
                @_item.CatalogType.Type
            </dd>
            <dt>
                Price
            </dt>

            <dd>
                @_item.Price
            </dd>

            <dt>
                Picture name
            </dt>

            <dd>
                @_item.PictureFileName
            </dd>

            <dt>
                Stock
            </dt>

            <dd>
                @_item.AvailableStock
            </dd>

            <dt>
                Restock
            </dt>

            <dd>
                @_item.RestockThreshold
            </dd>

            <dt>
                Max stock
            </dt>

            <dd>
                @_item.MaxStockThreshold
            </dd>

        </dl>
    </div>

    <div class="form-actions no-color esh-link-list">
        <a href="@($"/Catalog/Edit/{_item.Id}")" class="esh-link-item">
            Edit
        </a>
        |
        <a href="/" class="esh-link-item">
            Back to list
        </a>
    </div>

</div>

@code {
    private CatalogItem _item;

    [Parameter]
    public int Id { get; set; }

    protected override void OnInitialized()
    {
        Logger.LogInformation("Now loading... /Catalog/Details/{Id}", Id);

        _item = CatalogService.FindCatalogItem(Id);
    }
}

Beachten Sie, dass Code und Markup sich in derselben Datei befinden. Alle erforderlichen Dienste werden mit dem @inject-Attribut zugänglich gemacht. Gemäß @page-Anweisung kann über die Route Catalog/Details/{id} auf diese Seite zugegriffen werden. Der Wert des Platzhalters der Route {id} wurde auf eine Ganzzahl beschränkt. Wie im Abschnitt zum Routing beschrieben und im Gegensatz zu Web Forms, gibt eine Razor-Komponente ihre Route sowie sämtliche enthaltenen Parameter explizit an. Viele Web Forms-Steuerelemente haben möglicherweise keine exakten Pendants in Blazor. Häufig gibt es einen äquivalenten HTML-Codeausschnitt, der denselben Zweck erfüllt. Beispielsweise kann das Steuerelement <asp:Label /> durch ein <label>-HTML-Element ersetzt werden.

Modellvalidierung in Blazor

Wenn Ihr Web Forms-Code eine Validierung enthält, können Sie viele vorhandene Elemente ohne oder mit nur geringfügigen Änderungen übertragen. Ein Vorteil der Ausführung in Blazor besteht darin, dass dieselbe Validierungslogik verwendet werden kann, ohne dass benutzerdefiniertes JavaScript erforderlich ist. Datenanmerkungen erleichtern die Modellvalidierung.

Beispielsweise enthält die Seite Create.aspx ein Formular für die Dateneingabe mit Überprüfung. Ein Codeausschnitt könnte z. B. wie folgt aussehen:

<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>

In Blazor wird das äquivalente Markup in einer Create.razor-Datei bereitgestellt:

<EditForm Model="_item" OnValidSubmit="@...">
    <DataAnnotationsValidator />

    <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>

Der EditForm-Kontext umfasst Validierungsunterstützung und kann eine Eingabe umschließen. Datenanmerkungen sind eine gängige Methode zum Hinzufügen einer Validierung. Eine solche Validierungsunterstützung kann über die Komponente DataAnnotationsValidator hinzugefügt werden. Weitere Informationen zu diesem Mechanismus finden Sie unter Blazor-Formulare und -Validierung in ASP.NET Core.

Migrating Configuration (Migrieren der Konfiguration)

In einem Web Forms-Projekt werden Konfigurationsdaten zumeist in der web.config-Datei gespeichert. Der Zugriff auf die Konfigurationsdaten erfolgt mit ConfigurationManager. Zum Analysieren von Objekten wurden häufig Dienste benötigt. In .NET Framework 4.7.2 wurde der Konfiguration über ConfigurationBuilders die Möglichkeit von Zusammensetzungen hinzugefügt. Diese Generatoren ermöglichten Entwicklern das Hinzufügen verschiedener Quellen für die Konfiguration, die dann zur Laufzeit zusammengestellt wurde, um die erforderlichen Werte abzurufen.

In ASP.NET Core wurde ein flexibles Konfigurationssystem eingeführt, mit dem Sie die Konfigurationsquellen definieren können, die von Ihrer App und Bereitstellung verwendet werden. Die ConfigurationBuilder-Infrastruktur, die Sie in Ihrer Web Forms-App verwenden, wurde nach den im ASP.NET Core-Konfigurationssystem verwendeten Konzepten modelliert.

Der folgende Codeausschnitt zeigt, wie das Web Forms-eShop-Projekt die Datei web.config verwendet, um Konfigurationswerte zu speichern:

<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>

Geheimnisse wie Datenbank-Verbindungszeichenfolgen werden häufig in web.config gespeichert. Die Geheimnisse werden unweigerlich in unsicheren Speicherorten wie z. B. der Quellcodeverwaltung dauerhaft gespeichert. Mit Blazor in ASP.NET Core wird die frühere XML-basierte Konfiguration durch folgenden JSON-Code ersetzt:

{
  "ConnectionStrings": {
    "CatalogDBContext": "Data Source=(localdb)\\MSSQLLocalDB; Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb; Integrated Security=True; MultipleActiveResultSets=True;"
  },
  "UseMockData": true,
  "UseCustomizationData": false
}

JSON ist das Standardkonfigurationsformat. ASP.NET Core unterstützt jedoch viele weitere Formate, einschließlich XML. Es gibt außerdem verschiedene von der Community unterstützte Formate.

Sie können über den Generator in Program.csauf Konfigurationswerte zugreifen:

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")));
}

Standardmäßig werden Umgebungsvariablen, JSON-Dateien (appsettings.json und appsettings.{Environment}.json) und Befehlszeilenoptionen als gültige Konfigurationsquellen im Konfigurationsobjekt registriert. Der Zugriff auf die Konfigurationsquellen kann über Configuration[key] erfolgen. Eine fortgeschrittenere Methode besteht darin, die Konfigurationsdaten mithilfe des Optionsmusters an Objekte zu binden. Weitere Informationen zur Konfiguration und zum Optionsmuster finden Sie unter Konfiguration in ASP.NET Core bzw. Optionsmuster in ASP.NET Core.

Migrieren des Datenzugriffs

Der Datenzugriff ist ein wichtiger Aspekt jeder App. Das eShop-Projekt speichert Kataloginformationen in einer Datenbank und ruft die Daten mit Entity Framework 6 (EF 6) ab. Da EF 6 in .NET 5 unterstützt wird, kann diese Version vom Projekt weiterhin verwendet werden.

Für eShop waren die folgenden Änderungen in Bezug auf EF erforderlich:

  • In .NET Framework akzeptiert das DbContext-Objekt eine Zeichenfolge der Form name=ConnectionString und verwendet die Verbindungszeichenfolge aus ConfigurationManager.AppSettings[ConnectionString] zum Herstellen einer Verbindung. In .NET Core wird dieses Verhalten nicht unterstützt. Die Verbindungszeichenfolge muss angegeben werden.
  • Der Zugriff auf die Datenbank erfolgte synchron. Dies funktioniert zwar, aber möglicherweise zu Lasten der Skalierbarkeit. Diese Logik sollte in ein asynchrones Muster verlagert werden.

Obwohl nicht dieselbe native Unterstützung für die Datasetbindung vorhanden ist, bietet Blazor mit der C#-Unterstützung in einer Razor-Seite Flexibilität und Leistungsfähigkeit. Sie können z. B. Berechnungen durchführen und das Ergebnis anzeigen. Weitere Informationen zu Datenmustern in Blazor finden Sie im Kapitel Datenzugriff.

Änderungen der Architektur

Beim Migrieren zu Blazor sind einige wichtige Unterschiede in der Architektur zu berücksichtigen. Viele dieser Änderungen gelten für alles, was auf .NET Core oder ASP.NET Core basiert.

Da Blazor auf .NET Core aufbaut, gibt es Überlegungen zur Sicherstellung der Unterstützung in .NET Core. Einige der wichtigsten Änderungen betreffen die Entfernung der folgenden Features:

  • Mehrere AppDomains
  • Remoting
  • Codezugriffssicherheit (Code Access Security, CAS)
  • Sicherheitstransparenz

Weitere Informationen zu Techniken, mit denen sich die notwendigen Änderungen zur Unterstützung der Ausführung in .NET Core ermitteln lassen, finden Sie unter Portieren Ihres Codes von .NET Framework auf .NET Core.

ASP.NET Core ist eine neu gestaltete Version von ASP.NET und umfasst einige Änderungen, die nicht auf den ersten Blick ersichtlich sind. Dies sind die wichtigsten Änderungen:

  • Kein Synchronisierungskontext, was hier bedeutet, dass es weder HttpContext.Current noch Thread.CurrentPrincipal noch andere statische Accessoren gibt
  • Keine Schattenkopien
  • Keine Anforderungswarteschlange

Viele Vorgänge in ASP.NET Core laufen asynchron ab, was ein einfacheres Abladen von E/A-gebundenen Tasks ermöglicht. Es ist wichtig, niemals mit Task.Wait() oder Task.GetResult() zu blockieren, weil dadurch Threadpoolressourcen schnell erschöpft sein können.

Fazit

Sie haben hier viele Beispiele für die Aufgaben gesehen, die beim Verschieben eines Web Forms-Projekts nach Blazor ausgeführt werden müssen. Ein vollständiges Beispiel finden Sie im Projekt eShopOnBlazor.