Compartilhar via


Migrar do ASP.NET Web Forms para Blazor

Dica

Esse conteúdo é um trecho do livro eletrônico, Blazor para Desenvolvedores do ASP NET Web Forms para o Azure, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

BlazorImagem em miniaturada da capa do livro eletrônico para-Desenvolvedores-do-ASP-NET-Web-Forms.

Migrar uma base de código de ASP.NET Web Forms para Blazor uma tarefa demorada que requer planejamento. Este capítulo descreve o processo. Algo que pode facilitar a transição é garantir que o aplicativo siga uma arquitetura de N camadas, em que o modelo de aplicativo (nesse caso, Web Forms) é separado da lógica de negócios. Essa separação lógica de camadas deixa claro o que precisa ser movido para o .NET Core e Blazor.

Para este exemplo, o aplicativo eShop disponível no GitHub é usado. O eShop é um serviço de catálogo que fornece recursos CRUD por meio de entrada e validação de formulário.

Por que um aplicativo de trabalho deve ser migrado para Blazor? Muitas vezes, não há necessidade. O ASP.NET Web Forms ainda terá suporte por muitos anos. No entanto, muitos dos recursos fornecidos pelo Blazor só têm suporte em um aplicativo migrado. Os recursos incluem:

  • Melhorias de desempenho na estrutura, como Span<T>
  • Capacidade de executar como WebAssembly
  • Suporte multiplataforma para Linux e macOS
  • Implantação local do aplicativo ou implantação de estrutura compartilhada sem afetar outros aplicativos

Se esses ou outros novos recursos forem atraentes o suficiente, pode haver valor na migração do aplicativo. A migração pode assumir formas diferentes; pode ser o aplicativo inteiro ou apenas determinados pontos de extremidade que exigem as alterações. A decisão de migrar é, em última análise, baseada nos problemas comerciais a serem resolvidos pelo desenvolvedor.

Hospedagem do lado do servidor versus do lado do cliente

Conforme descrito no capítulo modelos de hospedagem, um aplicativo Blazor pode ser hospedado de duas maneiras diferentes: do lado do servidor e do lado do cliente. O modelo do lado do servidor usa conexões SignalR em ASP.NET Core para gerenciar as atualizações do DOM enquanto executa qualquer código real no servidor. O modelo do lado do cliente é executado como WebAssembly dentro de um navegador e não requer conexões de servidor. Há várias diferenças que podem afetar o que é melhor para um aplicativo específico:

  • No momento, executar como WebAssembly não dá suporte a todos os recursos (como threading)
  • A comunicação loquaz entre o cliente e o servidor pode causar problemas de latência no modo do servidor
  • O acesso a bancos de dados e serviços internos ou protegidos exige um serviço separado com hospedagem do lado do cliente

No momento da produção deste texto, o modelo do lado do servidor é mais parecido com o Web Forms. A maior parte deste capítulo se concentra no modelo de hospedagem do lado do servidor, pois ele está pronto para produção.

Criar um novo projeto

Essa etapa inicial de migração serve para criar um novo projeto. Esse tipo de projeto tem base nos projetos de estilo do SDK do .NET e simplifica grande parte do clichê usado em formatos de projeto anteriores. Para obter mais detalhes, confira o capítulo sobre Estrutura do Projeto.

Após a criação do projeto, instale as bibliotecas que foram usadas no projeto anterior. Em projetos de Web Forms mais antigos, você pode ter usado o arquivo packages.config para listar os pacotes NuGet necessários. No novo projeto no estilo SDK, packages.config foi substituído por elementos <PackageReference> no arquivo de projeto. Um benefício para essa abordagem é que todas as dependências são instaladas transitivamente. Você só lista as dependências de nível superior que importam para você.

Muitas das dependências que você está usando estão disponíveis para .NET, incluindo Entity Framework 6 e log4net. Se não houver nenhuma versão do .NET ou do .NET Standard disponível, a versão do .NET Framework poderá ser usada com frequência. Sua quilometragem pode variar. Qualquer API usada que não esteja disponível no .NET causa um erro de runtime. O Visual Studio notifica você sobre esses pacotes. Um ícone amarelo aparece no nó Referências do projeto no Gerenciador de Soluções.

No projeto de eShop com base em Blazor, é possível ver os pacotes instalados. Anteriormente, o arquivo packages.config listava todos os pacotes usados no projeto, resultando em um arquivo com quase 50 linhas de comprimento. Um trecho de 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>

O elemento <packages> inclui todas as dependências necessárias. É difícil identificar quais desses pacotes estão incluídos porque você precisa deles. Alguns elementos <package> são listados simplesmente para atender às necessidades de dependências necessárias.

O projeto Blazor lista as dependências necessárias dentro de um elemento <ItemGroup> no arquivo de projeto:

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

Um pacote NuGet que simplifica a vida útil de desenvolvedores Web Forms é o Pacote de Compatibilidade do Windows. Embora o .NET seja multiplataforma, alguns recursos só estão disponíveis no Windows. Os recursos específicos ao Windows são disponibilizados instalando o pacote de compatibilidade. Entre os exemplos desses recursos estão o Registro, o WMI e os Serviços de Diretório. O pacote adiciona cerca de 20 mil APIs e ativa muitos serviços com os quais você já pode ter familiaridade. O projeto do eShop não exige o pacote de compatibilidade; mas se seus projetos usarem recursos específicos do Windows, o pacote facilitará os esforços de migração.

Habilitar o processo de inicialização

O processo de inicialização para Blazor foi alterado de Web Forms e segue uma configuração semelhante para outros serviços do ASP.NET Core. Quando hospedados no lado do servidor, os componentes Razor são executados como parte de um aplicativo de ASP.NET Core normal. Quando hospedados no navegador com WebAssembly, os componentes Razor usam um modelo de hospedagem semelhante. A diferença é que os componentes são executados como um serviço separado de qualquer um dos processos de back-end. De qualquer forma, a inicialização é semelhante.

O arquivo Global.asax.cs é a página de inicialização padrão para projetos Web Forms. No projeto do eShop, esse arquivo configura o contêiner de IoC (Inversion of Control) e manipula os vários eventos de ciclo de vida do aplicativo ou da solicitação. Alguns desses eventos são tratados com middleware (como Application_BeginRequest). Outros eventos exigem a substituição de serviços específicos por meio da DI (injeção de dependência).

Por exemplo, o arquivo Global.asax.cs para eShop contém o seguinte código:

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

O arquivo anterior se torna o arquivo Program.cs no Blazor do lado do servidor:

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

Uma alteração significativa que você pode observar com relação ao Web Forms é a proeminência da DI (injeção de dependência). A DI tem sido um princípio norteador no design em ASP.NET Core. Ela dá suporte à personalização de quase todos os aspectos da estrutura de ASP.NET Core. Há até mesmo um provedor de serviços interno que pode ser usado para muitos cenários. Se houver a necessidade de mais personalização, ela poderá ter o suporte de muitos projetos da comunidade. Por exemplo, você pode levar adiante seu investimento na biblioteca de DI de terceiros.

No aplicativo eShop original, há alguma configuração para o gerenciamento de sessão. Como o Blazor do lado do servidor usa SignalR em ASP.NET Core para comunicação, o estado da sessão não tem suporte, pois as conexões podem ocorrer independentemente de um contexto HTTP. Um aplicativo que usa o estado de sessão exige uma nova arquitetura antes de ser executado como um aplicativo Blazor.

Para saber mais sobre a inicialização do aplicativo, confira Inicialização do aplicativo.

Migrar módulos HTTP e manipuladores para middleware

Módulos HTTP e manipuladores são padrões comuns em Web Forms que servem para controlar o pipeline de solicitações HTTP. Classes que implementam IHttpModule ou IHttpHandler podem ser registradas e processam solicitações de entrada. O Web Forms configura módulos e manipuladores no arquivo web.config. O Web Forms também tem muita base no tratamento de eventos do ciclo de vida do aplicativo. O ASP.NET Core usa, em vez disso, middleware. O middleware é registrado no método Configure da classe Startup. A ordem de execução do middleware é determinada pela ordem de registro.

Na seção Habilitar processo de inicialização, um evento de ciclo de vida foi gerado por Web Forms como o método Application_BeginRequest. Esse evento não está disponível no ASP.NET Core. Uma maneira de alcançar esse comportamento é implementar o middleware, como visto no exemplo de arquivo Startup.cs. Esse middleware executa a mesma lógica e, em seguida, transfere o controle para o próximo manipulador no pipeline do middleware.

Para saber mais sobre como migrar módulos e manipuladores, confira Migrar manipuladores e módulos HTTP para middleware do ASP.NET Core.

Migrar arquivos estáticos

Para atender a arquivos estáticos (por exemplo, HTML, CSS, imagens e JavaScript), os arquivos devem ser expostos pelo middleware. Chamar o método UseStaticFiles habilita o atendimento de arquivos estáticos a partir do caminho raiz da Web. O diretório raiz da Web padrão é wwwroot, mas pode ser personalizado. Conforme incluído no arquivo Program.cs:

...

app.UseStaticFiles();

...

O projeto eShop habilita o acesso básico a arquivos estáticos. Há muitas personalizações disponíveis para acesso a arquivos estáticos. Para obter informações sobre como habilitar arquivos padrão ou um navegador de arquivos, confira Arquivos estáticos em ASP.NET Core.

Migrar o agrupamento de runtime e a configuração de minificação

Agrupamento e minificação são técnicas de otimização de desempenho para redução do número e do tamanho das solicitações do servidor para recuperar determinados tipos de arquivo. JavaScript e CSS costumam passar por alguma forma de agrupamento ou minificação antes de serem enviados ao cliente. No ASP.NET Web Forms, essas otimizações são tratadas em tempo de execução. As convenções de otimização são definidas como um arquivo App_Start/BundleConfig.cs. No ASP.NET Core, adota-se uma abordagem mais declarativa. Um arquivo lista os arquivos a serem minificados, juntamente com configurações de minificação específicas.

Para saber mais sobre agrupamento e minificação, confira Agrupar e minificar ativos estáticos em ASP.NET Core.

Migrar páginas ASPX

Uma página em um aplicativo Web Forms é um arquivo com a extensão .aspx. Geralmente, uma página Web Forms pode ser mapeada para um componente no Blazor. Um componente Razor é criado em um arquivo com a extensão .razor. Para o projeto eShop, cinco páginas são convertidas em uma página Razor.

Por exemplo, a exibição de detalhes é composta por três arquivos no projeto Web Forms: Details.aspx, Details.aspx.cs e Details.aspx.designer.cs. Ao converter em Blazor, o code-behind e a marcação são combinados em Details.razor. A compilação Razor (equivalente ao que está em arquivos .designer.cs) é armazenada no diretório obj e não é, por padrão, acessível no Gerenciador de Soluções. A página Web Forms é composta pela seguinte marcação:

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

O code-behind da marcação anterior inclui o seguinte código:

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

Quando convertida emBlazor, a página Web Forms é traduzida para o seguinte código:

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

Observe que o código e a marcação estão no mesmo arquivo. Todos os serviços necessários são disponibilizados com o atributo @inject. De acordo com a diretiva @page, essa página pode ser acessada na rota Catalog/Details/{id}. O valor do espaço reservado {id} da rota foi restringido a um inteiro. Conforme descrito na seção roteamento, ao contrário do Web Forms, um componente Razor indica explicitamente sua rota e quaisquer parâmetros incluídos. Muitos controles do Web Forms podem não ter equivalentes exatos em Blazor. Geralmente, há um snippet HTML equivalente que servirá para o mesmo propósito. Por exemplo, o controle <asp:Label /> pode ser substituído por um elemento <label> HTML.

Validação de modelo no Blazor

Se o código do Web Forms incluir validação, você poderá transferir grande parte do que tem com o mínimo de alterações. Um benefício da execução no Blazor é que a mesma lógica de validação pode ser executada sem a necessidade de JavaScript personalizado. As anotações de dados permitem a validação fácil do modelo.

Por exemplo, a página Create.aspx tem um formulário de entrada de dados com validação. Este seria um exemplo de trecho de código:

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

No Blazor, a marcação equivalente é fornecida em um arquivo Create.razor:

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

O contexto EditForm inclui suporte à validação e pode ser encapsulado em torno de uma entrada. Anotações de dados são uma maneira comum de adicionar validação. Esse suporte de validação pode ser adicionado por meio do componente DataAnnotationsValidator. Para saber mais sobre esse mecanismo, confira Formulários e validação de Blazor no ASP.NET Core.

Migrar configuração

Em um projeto do Web Forms, os dados de configuração costumam ser armazenados no arquivo web.config. Os dados de configuração são acessados com ConfigurationManager. Frequentemente, os serviços precisavam analisar objetos. Com o .NET Framework 4.7.2, a composição foi adicionada à configuração por meio de ConfigurationBuilders. Esses construtores permitiram que os desenvolvedores adicionassem várias fontes à configuração que foi, então, composta em tempo de execução para recuperar os valores necessários.

O ASP.NET Core introduziu um sistema de configuração flexível que permite definir a fonte de configuração ou as fontes usadas pelo aplicativo e pela implantação. A infraestrutura ConfigurationBuilder que você pode estar usando em seu aplicativo Web Forms foi modelada com base nos conceitos usados no sistema de configuração do ASP.NET Core.

O trecho a seguir demonstra como o projeto de eShop do Web Forms usa web.config para armazenar valores de configuração:

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

É comum que segredos, como cadeias de conexão de banco de dados, sejam armazenados no web.config. Os segredos são inevitavelmente persistentes em locais não seguros, como o controle do código-fonte. No Blazor em ASP.NET Core, a configuração baseada em XML anterior é substituída pelo seguinte JSON:

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

JSON é o formato de configuração padrão; no entanto, ASP.NET Core dá suporte a muitos outros formatos, incluindo XML. Há também vários formatos com suporte da comunidade.

Você pode acessar valores de configuração a partir do construtor em Program.cs:

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

Por padrão, as variáveis de ambiente, arquivos JSON (appsettings.json e appsettings.{Environment}.json) e opções de linha de comando são registradas como fontes de configuração válidas no objeto de configuração. As fontes de configuração podem ser acessadas por meio de Configuration[key]. Uma técnica mais avançada é associar os dados de configuração a objetos usando o padrão de opções. Para saber mais sobre a configuração e o padrão de opções, confira Configuração no ASP.NET Core e Padrão de opções no ASP.NET Core, respectivamente.

Migrar o acesso a dados

O acesso a dados é um aspecto importante de qualquer aplicativo. O projeto eShop armazena informações de catálogo em um banco de dados e recupera os dados com o EF (Entity Framework) 6. Como o EF 6 tem suporte no .NET 5, o projeto pode continuar a usá-lo.

As seguintes alterações relacionadas ao EF foram necessárias para o eShop:

  • No .NET Framework, o objeto DbContext aceita uma cadeia de caracteres no formato name=ConnectionString e usa a cadeia de conexão de ConfigurationManager.AppSettings[ConnectionString] para se conectar. No .NET Core, não há suporte para isso. A cadeia de conexão deve ser fornecida.
  • O banco de dados foi acessado de forma síncrona. Embora isso funcione, a escalabilidade pode sofrer. Essa lógica deve ser movida para um padrão assíncrono.

Embora não haja o mesmo suporte nativo para associação de conjunto de dados, o Blazor fornece flexibilidade e potência com o suporte de C# em uma página Razor. Por exemplo, você pode executar cálculos e exibir o resultado. Para saber mais sobre padrões de dados no Blazor, confira o capítulo Acesso a dados.

Alterações na arquitetura

Por fim, há algumas diferenças importantes de arquitetura a serem consideradas ao migrar para Blazor. Muitas dessas alterações são aplicáveis a qualquer coisa com base no .NET Core ou no ASP.NET Core.

Como o Blazor tem base no .NET Core, há considerações para garantir o suporte no .NET Core. Algumas das principais alterações incluem a remoção dos seguintes recursos:

  • Vários AppDomains
  • Comunicação remota
  • CAS (segurança de acesso ao código)
  • Transparência de Segurança

Para saber mais sobre técnicas para identificar as alterações necessárias para dar suporte à execução no .NET Core, confira Portabilidade de seu código do .NET Framework para o .NET Core.

O ASP.NET Core é uma versão reimaginada do ASP.NET e tem algumas alterações que podem não parecer óbvias a princípio. As principais alterações são:

  • Nenhum contexto de sincronização, ou seja, sem HttpContext.Current, Thread.CurrentPrincipal ou outros acessadores estáticos
  • Sem cópia de sombra
  • Sem fila de solicitação

Muitas operações no ASP.NET Core são assíncronas, o que permite facilitar o descarregamento de tarefas associadas a E/S. É importante nunca bloquear usando Task.Wait() ou Task.GetResult(), que podem esgotar rapidamente os recursos do pool de threads.

Conclusão da migração

Neste ponto, você viu muitos exemplos do que é preciso para mover um projeto do Web Forms para o Blazor. Para ver um exemplo completo, confira o projeto eShopOnBlazor.