Compartir vía


Diseño de implementación para aplicaciones Blazor WebAssembly de ASP.NET Core hospedadas

En este artículo se explica cómo habilitar implementaciones de Blazor WebAssembly hospedadas en entornos que bloquean la descarga y ejecución de archivos de la biblioteca de vínculos dinámicos (DLL).

Nota:

Esta guía aborda los entornos que impiden que los clientes descarguen y ejecuten archivos DLL. En .NET 8 o posterior, Blazor usa el formato de archivo Webcil para solucionar este problema. Para obtener más información, consulta Hospedaje e implementación en ASP.NET Core Blazor WebAssembly. La agrupación de varias partes mediante el paquete NuGet experimental descrito en este artículo no es compatible con las aplicaciones Blazor de .NET 8 o posteriores. Puedes usar las instrucciones de este artículo para crear tu propio paquete NuGet de múltiples partes para .NET 8 o posterior.

Las aplicaciones Blazor WebAssembly requieren bibliotecas de vínculos dinámicos (DLL) para funcionar, pero algunos entornos bloquean a los clientes para que no descarguen y ejecuten archivos DLL. En un subconjunto de estos entornos, cambiar la extensión de nombre de archivo de los archivos DLL (.dll) es suficiente para omitir las restricciones de seguridad, pero los productos de seguridad a menudo pueden examinar el contenido de los archivos que atraviesan la red y bloquear o poner en cuarentena los archivos DLL. En este artículo se describe un enfoque para habilitar aplicaciones Blazor WebAssembly en estos entornos, donde se crea un archivo de agrupación de varias partes a partir de los archivos DLL de la aplicación para que los archivos DLL se puedan descargar juntos omitiendo las restricciones de seguridad.

Una aplicación Blazor WebAssembly hospedada puede personalizar sus archivos publicados y empaquetar archivos DLL de la aplicación mediante las siguientes características:

  • Inicializadores de JavaScript que permiten personalizar el proceso de arranque de Blazor.
  • Extensibilidad de MSBuild para transformar la lista de archivos publicados y definir las extensiones de publicación de Blazor . Las extensiones de publicación de Blazor son archivos definidos durante el proceso de publicación que proporcionan una representación alternativa para el conjunto de archivos necesarios para ejecutar una aplicación Blazor WebAssembly publicada. En este artículo, se crea una extensión de publicación de Blazor que genera un conjunto de varias partes con todos los archivos DLL de la aplicación empaquetados en un solo archivo para que los archivos DLL se puedan descargar juntos.

El enfoque que se muestra en este artículo sirve como punto de partida para que los desarrolladores puedan diseñar sus propias estrategias y procesos de carga personalizados.

Advertencia

Cualquier enfoque que se tome para evitar una restricción de seguridad debe tenerse en cuenta cuidadosamente por sus implicaciones de seguridad. Se recomienda explorar el tema con los profesionales de seguridad de red de la organización antes de adoptar el enfoque de este artículo. Entre las alternativas a tener en cuenta se incluyen las siguientes:

  • Habilitar dispositivos y software de seguridad para permitir que los clientes de red descarguen y usen los archivos exactos que requiere una aplicación Blazor WebAssembly.
  • Cambia del modelo de hospedaje Blazor WebAssembly al modelo de hospedaje Blazor Server, que mantiene todo el código de C# de la aplicación en el servidor y no requiere que los clientes descarguen archivos DLL. Blazor Server también ofrece la ventaja de mantener el código de C# privado sin requerir el uso de aplicaciones de API web para privacy del código de C# con aplicaciones Blazor WebAssembly.

Aplicación de ejemplo y paquete NuGet experimental

El enfoque descrito en este artículo lo usa el paquete experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) para aplicaciones destinadas a .NET 6 o posterior. El paquete contiene destinos de MSBuild para personalizar la salida de publicación de Blazor y un inicializador de JavaScript para usar un cargador de recursos de arranque personalizado, cada uno de los cuales se describe en detalle más adelante en este artículo.

Código experimental (incluye el origen de referencia del paquete NuGet y la aplicación de ejemplo CustomPackagedApp)

Advertencia

Las características experimentales y en versión preliminar se proporcionan con el fin de recopilar comentarios y no se admiten para su uso en producción.

Más adelante en este artículo, la sección Personalización del proceso de carga de Blazor WebAssembly a través de un paquete NuGet con sus tres subsecciones proporciona explicaciones detalladas sobre la configuración y el código del paquete Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Las explicaciones detalladas son importantes para comprender cuándo se crea su propia estrategia y el proceso de carga personalizado para las aplicaciones Blazor WebAssembly. Para usar el paquete NuGet experimental, no compatible publicado sin personalización como demostración local, realiza los pasos siguientes:

  1. Usa una solución de Blazor WebAssembly hospedada existente o cree una solución a partir de la plantilla de proyecto de Blazor WebAssembly mediante Visual Studio o pasando la opción -ho|--hosted al comando dotnet new (dotnet new blazorwasm -ho). Para obtener más información, consulta Herramientas para ASP.NET Core Blazor.

  2. En el proyecto Client, agrega el paquete Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle experimental:

    Nota:

    Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulta los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (documentación de NuGet). Confirma las versiones correctas del paquete en NuGet.org.

  3. En el proyecto Server , agrega un punto de conexión para proporcionar el archivo de agrupación (app.bundle). Puedes encontrar código de ejemplo en la sección Envío del paquete desde la aplicación de servidor host de este artículo.

  4. Publica la aplicación en Configuración de versión.

Personalización del proceso de carga Blazor WebAssembly a través del paquete NuGet

Advertencia

Las instrucciones de esta sección con sus tres subsecciones pertenecen a la creación de un paquete NuGet desde cero para implementar su propia estrategia y un proceso de carga personalizado. El paquete experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) para .NET 6 y 7 se basa en las directrices de esta sección. Al usar el paquete proporcionado en una demostración local del enfoque de descarga del conjunto de varias partes, no es necesario seguir las instrucciones de esta sección. Para obtener instrucciones sobre cómo usar el paquete proporcionado, consulta la sección Aplicación de ejemplo y paquete NuGet experimental.

Los recursos de la aplicación Blazor se empaquetan en un archivo de agrupación de varias partes y el explorador los carga a través de un inicializador de JavaScript (JS)JS personalizado. Para una aplicación que consume el paquete con el inicializador de JS, la aplicación solo requiere que el archivo de agrupación se envíe cuando se solicite. Todos los demás aspectos de este enfoque se controlan de forma transparente.

Se requieren cuatro personalizaciones para la carga de una aplicación Blazor publicada predeterminada:

  • Una tarea de MSBuild para transformar los archivos de publicación.
  • Un paquete NuGet con destinos de MSBuild que se conecta al proceso de publicación de Blazor, transforma la salida y define uno o varios archivos de extensión de publicación de Blazor (en este caso, una única agrupación).
  • Inicializador de JS para actualizar la devolución de llamada del cargador de recursos de Blazor WebAssembly para que cargue el paquete y proporciona a la aplicación los archivos individuales.
  • Un asistente en la aplicación host Server para asegurarse de que la agrupación se proporciona a los clientes a petición.

Creación de una tarea de MSBuild para personalizar la lista de archivos publicados y definir nuevas extensiones

Crea una tarea de MSBuild como una clase pública de C# que se pueda importar como parte de una compilación de MSBuild y que pueda interactuar con la compilación.

Se requiere lo siguiente para la clase de C#:

Nota:

El paquete NuGet de los ejemplos de este artículo reciben el nombre del paquete proporcionado por Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Para obtener instrucciones sobre cómo asignar nombres y producir su propio paquete NuGet, consulta los siguientes artículos de 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>

Determina las versiones más recientes del paquete para los marcadores de posición de {VERSION} en NuGet.org:

Para crear la tarea de MSBuild, crea una clase pública de C# que extienda Microsoft.Build.Utilities.Task (no System.Threading.Tasks.Task) y declara tres propiedades:

  • PublishBlazorBootStaticWebAsset: lista de archivos que se publicarán para la aplicación Blazor.
  • BundlePath: ruta de acceso donde se escribe la agrupación.
  • Extension: nuevas extensiones de publicación que se incluirán en la compilación.

La siguiente clase de ejemplo de BundleBlazorAssets es un punto de partida para una mayor personalización:

  • En el método Execute, la agrupación se crea a partir de los tres tipos de archivo siguientes:
    • Archivos de JavaScript (dotnet.js)
    • Archivos WASM (dotnet.wasm)
    • Archivos DLL de aplicación (.dll)
  • Se crea una agrupación multipart/form-data. Cada archivo se agrega a la agrupación con sus correspondientes descripciones a través de los encabezados Content-Disposition y Content-Type.
  • Una vez creada la agrupación, se escribe en un archivo.
  • La compilación se configura para la extensión. En el código siguiente se crea un elemento de extensión y se agrega a la propiedad Extension. Cada elemento de extensión contiene tres fragmentos de datos:
    • Ruta de acceso al archivo de extensión.
    • Ruta de acceso URL relativa a la raíz de la aplicación Blazor WebAssembly.
    • Nombre de la extensión, que agrupa los archivos generados por una extensión determinada.

Después de lograr los objetivos anteriores, se crea la tarea de MSBuild para personalizar la salida de publicación de Blazor. Blazor se encarga de recopilar las extensiones y de asegurarse de que se copian en la ubicación correcta de la carpeta de salida de publicación (por ejemplo, bin\Release\net6.0\publish). Las mismas optimizaciones (por ejemplo, compresión) se aplican a los archivos JavaScript, WASM y DLL que se aplican a otros archivos Blazor.

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

Creación de un paquete NuGet para transformar automáticamente la salida de la publicación

Genera un paquete NuGet con destinos de MSBuild que se incluyan automáticamente cuando se haga referencia al paquete:

  • Crea un proyecto de Biblioteca de clases Razor (RCL).
  • Crea un archivo de destinos siguiendo convenciones NuGet para importar automáticamente el paquete en proyectos de consumo. Por ejemplo, crea build\net6.0\{PACKAGE ID}.targets, donde {PACKAGE ID} es el identificador de paquete del paquete.
  • Recopila la salida de la biblioteca de clases que contiene la tarea de MSBuild y confirma que la salida está empaquetada en la ubicación correcta.
  • Agrega el código de MSBuild necesario para adjuntar a la canalización Blazor e invoca la tarea de MSBuild para generar la agrupación.

El enfoque descrito en esta sección solo usa el paquete para entregar destinos y contenido, que es diferente de la mayoría de los paquetes en los que el paquete incluye un archivo DLL de biblioteca.

Advertencia

El paquete de ejemplo descrito en esta sección muestra cómo personalizar el proceso de publicación de Blazor. El paquete NuGet de ejemplo solo se usa como demostración local. No se admite el uso de este paquete en producción.

Nota:

El paquete NuGet de los ejemplos de este artículo reciben el nombre del paquete proporcionado por Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Para obtener instrucciones sobre cómo asignar nombres y producir su propio paquete NuGet, consulta los siguientes artículos de 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>

Nota:

La propiedad <NoWarn>NU5100</NoWarn> del ejemplo anterior suprime la advertencia sobre los ensamblados colocados en la carpeta tasks. Para obtener más información, consulta la advertencia de NuGet NU5100.

Agrega un archivo .targets para conectar la tarea MSBuild a la canalización de compilación. En este archivo, se logran los siguientes objetivos:

  • Importar la tarea en el proceso de compilación. Ten en cuenta que la ruta de acceso al archivo DLL es relativa a la ubicación final del archivo en el paquete.
  • La propiedad ComputeBlazorExtensionsDependsOn asocia el destino personalizado a la canalización Blazor WebAssembly.
  • Captura la propiedad Extension en la salida de la tarea y agrégala a BlazorPublishExtension para que se informe a Blazor sobre la extensión. La invocación de la tarea en el destino genera la agrupación. La canalización Blazor WebAssembly del grupo de elementos PublishBlazorBootStaticWebAsset proporciona la lista de archivos publicados. La ruta de acceso de agrupación se define mediante IntermediateOutputPath (normalmente dentro de la carpeta obj). En última instancia, la agrupación se copia automáticamente en la ubicación correcta de la carpeta de salida de publicación (por ejemplo, bin\Release\net6.0\publish).

Cuando se hace referencia al paquete, genera una agrupación de los archivos Blazor durante la publicación.

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>

Arranque automático Blazor desde la agrupación

El paquete NuGet utiliza inicializadores de JavaScript (JS) para arrancar automáticamente una aplicación Blazor WebAssembly desde el paquete en lugar de usar archivos DLL individuales. Los inicializadores de JS se usan para cambiar el cargador de recursos de arranque de Blazor y usar el paquete.

Para crear un inicializador de JS, agrega un archivo de JS con el nombre {NAME}.lib.module.js a la carpeta wwwroot del proyecto de paquete, donde el marcador de posición {NAME} es el identificador del paquete. Por ejemplo, el archivo del paquete de Microsoft se denomina Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Las funciones exportadas beforeWebAssemblyStart y afterWebAssemblyStarted controlan la carga.

Los inicializadores de 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 crear un inicializador de JS, agrega un archivo de JS con el nombre {NAME}.lib.module.js a la carpeta wwwroot del proyecto de paquete, donde el marcador de posición {NAME} es el identificador del paquete. Por ejemplo, el archivo del paquete de Microsoft se denomina Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Las funciones exportadas beforeStart y afterStarted controlan la carga.

Los inicializadores de 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);
  }
}

Envío del paquete desde la aplicación de servidor host

Debido a las restricciones de seguridad, ASP.NET Core no envía el archivo app.bundle. Se requiere un asistente de procesamiento de solicitudes para enviar el archivo cuando lo soliciten los clientes.

Nota:

Puesto que las mismas optimizaciones se aplican de forma transparente a las extensiones de publicación que se aplican a los archivos de la aplicación, los archivos de recursos comprimidos app.bundle.gz y app.bundle.br se generan automáticamente al publicar.

Coloque el código de C# en Program.cs del proyecto Server inmediatamente antes de la línea que establece el archivo de reserva en index.html (app.MapFallbackToFile("index.html");) para responder a una solicitud del archivo de agrupación (por ejemplo, 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);
});

El tipo de contenido coincide con el tipo definido anteriormente en la tarea de compilación. El punto de conexión comprueba las codificaciones de contenido aceptadas por el explorador y envía el archivo óptimo, Brotli (.br) o Gzip (.gz).