Partilhar via


Layout de implantação para aplicativos hospedados no ASP.NET Core Blazor WebAssembly

Esse artigo explica como habilitar implantações Blazor WebAssembly hospedadas em ambientes que bloqueiam o download e a execução de arquivos DLL (biblioteca de vínculo dinâmico).

Observação

Essa orientação aborda ambientes que impedem os clientes de baixar e executar DLLs. No .NET 8 ou posterior, Blazor usa o formato de arquivo Webcil para resolver esse problema. Para obter mais informações, confira Hospedar e implantar Blazor WebAssembly do ASP.NET Core. O agrupamento de várias partes usando o pacote NuGet experimental descrito por esse artigo não tem suporte para aplicativos do Blazor no .NET 8 ou posterior. Você pode usar as diretrizes neste artigo para criar seu próprio pacote NuGet de agrupamento de várias partes para .NET 8 ou posterior.

Os aplicativos Blazor WebAssembly exigem DLLs (bibliotecas de link dinâmico) para funcionar, mas alguns ambientes impedem que os clientes baixem e executem DLLs. Em um subconjunto desses ambientes, alterar a extensão de nome de arquivo de arquivos DLL (.dll) é suficiente para ignorar as restrições de segurança, mas os produtos de segurança geralmente são capazes de verificar o conteúdo dos arquivos que atravessam a rede e bloquear ou colocar arquivos DLL em quarentena. Este artigo descreve uma abordagem para habilitar aplicativos Blazor WebAssembly nesses ambientes, em que um arquivo de pacote de várias partes é criado a partir das DLLs do aplicativo para que as DLLs possam ser baixadas juntas ignorando as restrições de segurança.

Um aplicativo Blazor WebAssembly hospedado pode personalizar seus arquivos publicados e empacotar DLLs de aplicativo usando os seguintes recursos:

  • Inicializadores JavaScript que permitem personalizar o processo de inicialização do Blazor.
  • Extensibilidade do MSBuild para transformar a lista de arquivos publicados e definir Blazor Extensões de Publicação. As Extensões de Publicação Blazor são arquivos definidos durante o processo de publicação que fornecem uma representação alternativa para o conjunto de arquivos necessários para executar um aplicativo Blazor WebAssembly publicado. Neste artigo, uma Extensão de Publicação Blazor é criada que produz um pacote de várias partes com todas as DLLs do aplicativo empacotadas em um único arquivo para que as DLLs possam ser baixadas juntas.

A abordagem demonstrada neste artigo serve como um ponto de partida para os desenvolvedores elaborarem suas próprias estratégias e processos de carregamento personalizados.

Aviso

Qualquer abordagem tomada para contornar uma restrição de segurança deve ser cuidadosamente considerada por suas implicações de segurança. Recomendamos explorar ainda mais o assunto com os profissionais de segurança de rede da sua organização antes de adotar a abordagem neste artigo. As alternativas a serem consideradas incluem:

  • Habilite dispositivos de segurança e software de segurança para permitir que os clientes de rede baixem e usem os arquivos exatos exigidos por um aplicativo Blazor WebAssembly.
  • Alterne do modelo de hospedagem Blazor WebAssembly para o modelo de hospedagem do Blazor Server, que mantém todo o código C# do aplicativo no servidor e não requer o download de DLLs para clientes. Blazor Server também oferece a vantagem de manter o código C# privado sem exigir o uso de aplicativos de API Web para privacy de código C# com aplicativos Blazor WebAssembly.

Pacote NuGet experimental e aplicativo de exemplo

A abordagem descrita neste artigo é usada pelo pacote experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) para aplicativos direcionados ao .NET 6 ou posterior. O pacote contém destinos do MSBuild para personalizar a saída de publicação Blazor e um inicializador JavaScript para usar um carregador de recursos de inicialização personalizado, cada um dos quais são descritos em detalhes mais adiante neste artigo.

Código experimental (inclui a origem do pacote NuGet e o aplicativo de exemplo CustomPackagedApp)

Aviso

Os recursos experimentais e de versão prévia são fornecidos com a finalidade de coletar comentários e não têm suporte para uso em produção.

Posteriormente neste artigo, a seção Personalizar o processo de carregamento do Blazor WebAssembly por meio de um pacote NuGet com suas três subseções fornece explicações detalhadas sobre a configuração e o código no pacote Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. As explicações detalhadas são importantes para entender quando você cria sua própria estratégia e processo de carregamento personalizado para os aplicativos Blazor WebAssembly. Para usar o pacote NuGet publicado, experimental e sem suporte sem personalização como uma demonstração local, execute as seguintes etapas:

  1. Use uma Blazor WebAssemblysolução hospedada existente ou crie uma nova solução com base no modelo de projeto usando o Blazor WebAssembly Visual Studio ou passando a opção-ho|--hosted para o comando dotnet new (dotnet new blazorwasm -ho). Para obter mais informações, confira Ferramentas para ASP.NET Core Blazor.

  2. No projeto Client, adicione o pacote experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.

    Observação

    Para obter diretrizes sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes no Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas de pacote em NuGet.org.

  3. No projeto Server, adicione um ponto de extremidade para servir o arquivo de pacote (app.bundle). O código de exemplo pode ser encontrado na seção Servir o pacote do aplicativo do servidor host deste artigo.

  4. Publique o aplicativo na Configuração de versão.

Personalizar o processo de carregamento do Blazor WebAssembly por meio de um pacote NuGet

Aviso

As diretrizes nesta seção com suas três subseções referem-se à criação de um pacote NuGet do zero para implementar sua própria estratégia e processo de carregamento personalizado. O pacote experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) para .NET 6 e 7 baseia-se nas diretrizes dessa seção. Ao usar o pacote fornecido em uma demonstração local da abordagem de download de pacote de várias partes, você não precisa seguir as diretrizes nesta seção. Para obter diretrizes sobre como usar o pacote fornecido, consulte a seção Pacote NuGet experimental e aplicativo de exemplo.

Os recursos do aplicativo Blazor são empacotados em um arquivo de pacote de várias partes e carregados pelo navegador por meio de um inicializador JavaScript (JS) personalizado. Para um aplicativo que consome o pacote com o inicializador JS, o aplicativo requer apenas que o arquivo de pacote seja atendido quando solicitado. Todos os outros aspectos dessa abordagem são tratados de forma transparente.

Quatro personalizações são necessárias para a forma como um aplicativo Blazor publicado e padrão carrega:

  • Uma tarefa do MSBuild para transformar os arquivos de publicação.
  • Um pacote NuGet com MSBuild é direcionado que se conecta ao Blazor processo de publicação, transforma a saída e define um ou mais arquivos de Extensão de Publicação Blazor (nesse caso, um único pacote).
  • Um JS inicializador para atualizar o retorno de chamada do carregador de recursos Blazor WebAssembly para que ele carregue o pacote e forneça ao aplicativo os arquivos individuais.
  • Um auxiliar no aplicativo host Server para garantir que o pacote seja atendido aos clientes sob solicitação.

Criar uma tarefa do MSBuild para personalizar a lista de arquivos publicados e definir novas extensões

Crie uma tarefa do MSBuild como uma classe C# pública que pode ser importada como parte de uma compilação do MSBuild e que possa interagir com o build.

Os seguintes itens são necessários para a classe C#:

Observação

O pacote NuGet para os exemplos deste artigo tem o nome do pacote fornecido pela Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Para obter diretrizes sobre como nomear e produzir seu próprio pacote NuGet, consulte os seguintes artigos do NuGet:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Framework" Version="{VERSION}" />
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="{VERSION}" />
  </ItemGroup>

</Project>

Determine as versões mais recentes do pacote para os espaços reservados {VERSION} em NuGet.org:

Para criar a tarefa MSBuild, crie uma classe C# pública estendendo Microsoft.Build.Utilities.Task (não System.Threading.Tasks.Task) e declare três propriedades:

  • PublishBlazorBootStaticWebAsset: a lista de arquivos a serem publicados para o aplicativo Blazor.
  • BundlePath: o caminho em que o pacote é gravado.
  • Extension: as novas Extensões de Publicação a serem incluídas no build.

A classe de exemplo BundleBlazorAssets a seguir é um ponto de partida para personalização adicional:

  • No método Execute, o pacote é criado com base nos três tipos de arquivo a seguir:
    • Arquivos JavaScript (dotnet.js)
    • Arquivos WASM (dotnet.wasm)
    • DLLs de aplicativo (.dll)
  • Um pacote multipart/form-data é criado. Cada arquivo é adicionado ao pacote com suas respectivas descrições por meio do cabeçalho Content-Disposition e do cabeçalho Content-Type.
  • Depois que o pacote é criado, o pacote é gravado em um arquivo.
  • O build está configurado para a extensão. O código a seguir cria um item de extensão e o adiciona à propriedade Extension. Cada item de extensão contém três partes de dados:
    • O caminho para o arquivo de extensão.
    • O caminho da URL relativo à raiz do aplicativo Blazor WebAssembly.
    • O nome da extensão, que agrupa os arquivos produzidos por uma determinada extensão.

Depois de atingir as metas anteriores, a tarefa MSBuild é criada para personalizar a saída de publicação Blazor. Blazor cuida da coleta das extensões e certificação de que as extensões sejam copiadas para o local correto na pasta de saída de publicação (por exemplo, bin\Release\net6.0\publish). As mesmas otimizações (por exemplo, compactação) são aplicadas aos arquivos JavaScript, WASM e DLL, conforme o Blazor se aplica a outros arquivos.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/BundleBlazorAssets.cs:

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks
{
    public class BundleBlazorAssets : Task
    {
        [Required]
        public ITaskItem[]? PublishBlazorBootStaticWebAsset { get; set; }

        [Required]
        public string? BundlePath { get; set; }

        [Output]
        public ITaskItem[]? Extension { get; set; }

        public override bool Execute()
        {
            var bundle = new MultipartFormDataContent(
                "--0a7e8441d64b4bf89086b85e59523b7d");

            foreach (var asset in PublishBlazorBootStaticWebAsset)
            {
                var name = Path.GetFileName(asset.GetMetadata("RelativePath"));
                var fileContents = File.OpenRead(asset.ItemSpec);
                var content = new StreamContent(fileContents);
                var disposition = new ContentDispositionHeaderValue("form-data");
                disposition.Name = name;
                disposition.FileName = name;
                content.Headers.ContentDisposition = disposition;
                var contentType = Path.GetExtension(name) switch
                {
                    ".js" => "text/javascript",
                    ".wasm" => "application/wasm",
                    _ => "application/octet-stream"
                };
                content.Headers.ContentType = 
                    MediaTypeHeaderValue.Parse(contentType);
                bundle.Add(content);
            }

            using (var output = File.Open(BundlePath, FileMode.OpenOrCreate))
            {
                output.SetLength(0);
                bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
                    .GetResult();
                output.Flush(true);
            }

            var bundleItem = new TaskItem(BundlePath);
            bundleItem.SetMetadata("RelativePath", "app.bundle");
            bundleItem.SetMetadata("ExtensionName", "multipart");

            Extension = new ITaskItem[] { bundleItem };

            return true;
        }
    }
}

Criar um pacote NuGet para transformar automaticamente a saída de publicação

Gere um pacote NuGet com destinos do MSBuild que são incluídos automaticamente quando o pacote é referenciado:

  • Crie um novo projeto de Biblioteca de Classes (RCL)Razor.
  • Crie um arquivo de destinos seguindo as convenções do NuGet para importar automaticamente o pacote no consumo de projetos. Por exemplo, crie o build\net6.0\{PACKAGE ID}.targets, onde {PACKAGE ID} apresenta o identificador de pacote do pacote.
  • Colete a saída da biblioteca de classes que contém a tarefa do MSBuild e confirme se a saída está empacotada no local certo.
  • Adicione o código MSBuild necessário para anexar ao pipeline Blazor e invocar a tarefa MSBuild para gerar o pacote.

A abordagem descrita nesta seção usa apenas o pacote para fornecer destinos e conteúdo, que é diferente da maioria dos pacotes em que o pacote inclui uma DLL de biblioteca.

Aviso

O pacote de exemplo descrito nesta seção demonstra como personalizar o processo de publicação do Blazor. O pacote NuGet de exemplo é usado apenas como uma demonstração local. Não há suporte para o uso desse pacote em produção.

Observação

O pacote NuGet para os exemplos deste artigo tem o nome do pacote fornecido pela Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Para obter diretrizes sobre como nomear e produzir seu próprio pacote NuGet, consulte os seguintes artigos do NuGet:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.csproj:

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <NoWarn>NU5100</NoWarn>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Description>
      Sample demonstration package showing how to customize the Blazor publish 
      process. Using this package in production is not supported!
    </Description>
    <IsPackable>true</IsPackable>
    <IsShipping>true</IsShipping>
    <IncludeBuildOutput>false</IncludeBuildOutput>
  </PropertyGroup>

  <ItemGroup>
    <None Update="build\**" 
          Pack="true" 
          PackagePath="%(Identity)" />
    <Content Include="_._" 
             Pack="true" 
             PackagePath="lib\net6.0\_._" />
  </ItemGroup>

  <Target Name="GetTasksOutputDlls" 
          BeforeTargets="CoreCompile">
    <MSBuild Projects="..\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj" 
             Targets="Publish;PublishItemsOutputGroup" 
             Properties="Configuration=Release">
      <Output TaskParameter="TargetOutputs" 
              ItemName="_TasksProjectOutputs" />
    </MSBuild>
    <ItemGroup>
      <Content Include="@(_TasksProjectOutputs)" 
               Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'" 
               Pack="true" 
               PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)" 
               KeepMetadata="Pack;PackagePath" />
    </ItemGroup>
  </Target>

</Project>

Observação

A propriedade <NoWarn>NU5100</NoWarn> no exemplo anterior suprime o aviso sobre os assemblies colocados na pasta tasks. Para obter mais informações, consulte Aviso NuGet NU5100.

Adicione um arquivo .targets para conectar a tarefa MSBuild ao pipeline de build. Neste arquivo, as seguintes metas são realizadas:

  • Importe a tarefa para o processo de build. Observe que o caminho para a DLL é relativo ao local final do arquivo no pacote.
  • A propriedade ComputeBlazorExtensionsDependsOn anexa o destino personalizado ao pipeline Blazor WebAssembly.
  • Capture a propriedade Extension na saída da tarefa e adicione-a para BlazorPublishExtension informar Blazor sobre a extensão. Invocar a tarefa no destino produz o pacote. A lista de arquivos publicados é fornecida pelo Blazor WebAssembly pipeline PublishBlazorBootStaticWebAsset no grupo de itens. O caminho do pacote é definido usando o IntermediateOutputPath (normalmente dentro da pasta obj). Por fim, o pacote é copiado automaticamente para o local correto na pasta de saída de publicação (por exemplo, bin\Release\net6.0\publish).

Quando o pacote é referenciado, ele gera um pacote dos arquivos Blazor durante a publicação.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/build/net6.0/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.targets:

<Project>
  <UsingTask 
    TaskName="Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.BundleBlazorAssets" 
    AssemblyFile="$(MSBuildThisProjectFileDirectory)..\..\tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.dll" />

  <PropertyGroup>
    <ComputeBlazorExtensionsDependsOn>
      $(ComputeBlazorExtensionsDependsOn);_BundleBlazorDlls
    </ComputeBlazorExtensionsDependsOn>
  </PropertyGroup>

  <Target Name="_BundleBlazorDlls">
    <BundleBlazorAssets
      PublishBlazorBootStaticWebAsset="@(PublishBlazorBootStaticWebAsset)"
      BundlePath="$(IntermediateOutputPath)bundle.multipart">
      <Output TaskParameter="Extension" 
              ItemName="BlazorPublishExtension"/>
    </BundleBlazorAssets>
  </Target>

</Project>

Inicializar o Blazor automaticamente do pacote

O pacote NuGet aproveita os inicializadores JavaScript (JS) para inicializar automaticamente um aplicativo Blazor WebAssembly do pacote em vez de usar arquivos DLL individuais. Os inicializadores JS são usados para alterar o Blazorcarregador de recursos de inicialização e para usar o pacote.

Para criar um inicializador JS, adicione um arquivo JS com o nome {NAME}.lib.module.js à pastawwwroot do projeto do pacote, em que o espaço reservado {NAME} é o identificador do pacote. Por exemplo, o arquivo para o pacote da Microsoft é o Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js nomeado. As funções exportadas beforeWebAssemblyStart e afterWebAssemblyStarted lidam com o carregamento.

Os inicializadores JS:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js:

const resources = new Map();

export async function beforeWebAssemblyStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterWebAssemblyStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

Para criar um inicializador JS, adicione um arquivo JS com o nome {NAME}.lib.module.js à pastawwwroot do projeto do pacote, em que o espaço reservado {NAME} é o identificador do pacote. Por exemplo, o arquivo para o pacote da Microsoft é o Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js nomeado. As funções exportadas beforeStart e afterStarted lidam com o carregamento.

Os inicializadores JS:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js:

const resources = new Map();

export async function beforeStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

Fornecer o pacote do aplicativo do servidor host

Devido a restrições de segurança, o ASP.NET Core não atende ao arquivo app.bundle. Um auxiliar de processamento de solicitação é necessário para atender ao arquivo quando ele é solicitado pelos clientes.

Observação

Como as mesmas otimizações são aplicadas de forma transparente às Extensões de Publicação aplicadas aos arquivos do aplicativo, os arquivos de ativo compactados app.bundle.gz e app.bundle.br são produzidos automaticamente na publicação.

Coloque o código C# no Program.cs do projeto Server imediatamente antes da linha que define o arquivo de fallback como index.html (app.MapFallbackToFile("index.html");) para responder a uma solicitação para o arquivo de pacote (por exemplo, app.bundle):

app.MapGet("app.bundle", (HttpContext context) =>
{
    string? contentEncoding = null;
    var contentType = 
        "multipart/form-data; boundary=\"--0a7e8441d64b4bf89086b85e59523b7d\"";
    var fileName = "app.bundle";

    var acceptEncodings = context.Request.Headers.AcceptEncoding;

    if (Microsoft.Net.Http.Headers.StringWithQualityHeaderValue
        .StringWithQualityHeaderValue
        .TryParseList(acceptEncodings, out var encodings))
    {
        if (encodings.Any(e => e.Value == "br"))
        {
            contentEncoding = "br";
            fileName += ".br";
        }
        else if (encodings.Any(e => e.Value == "gzip"))
        {
            contentEncoding = "gzip";
            fileName += ".gz";
        }
    }

    if (contentEncoding != null)
    {
        context.Response.Headers.ContentEncoding = contentEncoding;
    }

    return Results.File(
        app.Environment.WebRootFileProvider.GetFileInfo(fileName)
            .CreateReadStream(), contentType);
});

O tipo de conteúdo corresponde ao tipo definido anteriormente na tarefa de build. O ponto de extremidade verifica as codificações de conteúdo aceitas pelo navegador e atende ao arquivo ideal, Brotli (.br) ou Gzip (.gz).