Układ wdrażania dla aplikacji hostowanych Blazor WebAssembly ASP.NET Core

W tym artykule wyjaśniono, jak włączyć Blazor WebAssembly hostowane wdrożenia w środowiskach, które blokują pobieranie i wykonywanie plików biblioteki dynamicznego łącza (DLL).

Uwaga

Te wskazówki dotyczą środowisk, które blokują klientom pobieranie i wykonywanie bibliotek DLL. W programie .NET 8 lub nowszym Blazor format pliku Webcil jest używany do rozwiązania tego problemu. Aby uzyskać więcej informacji, zobacz Host and deploy ASP.NET Core Blazor WebAssembly. Tworzenie pakietów wielopartowych przy użyciu eksperymentalnego pakietu NuGet opisanego w tym artykule nie jest obsługiwane w przypadku Blazor aplikacji na platformie .NET 8 lub nowszej. Aby uzyskać więcej informacji, zobacz Ulepszanie pakietu Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle w celu zdefiniowania niestandardowego formatu pakietu (dotnet/aspnetcore #36978). Skorzystaj ze wskazówek w tym artykule, aby utworzyć własny wieloczęściowy pakiet NuGet dla platformy .NET 8 lub nowszej.

Blazor WebAssembly aplikacje wymagają , aby biblioteki linków dynamicznych (DLL) działały, ale niektóre środowiska blokują klientom pobieranie i wykonywanie bibliotek DLL. W podzestawie tych środowisk zmiana rozszerzenia nazwy pliku plików DLL (.dll) jest wystarczająca do obejścia ograniczeń zabezpieczeń, ale produkty zabezpieczeń są często w stanie skanować zawartość plików przechodzących przez sieć i zablokować lub poddać kwarantannie pliki DLL. W tym artykule opisano jedno podejście do włączania Blazor WebAssembly aplikacji w tych środowiskach, w których jest tworzony plik pakietu wieloczęściowego na podstawie bibliotek DLL aplikacji, dzięki czemu biblioteki DLL można pobrać razem, pomijając ograniczenia zabezpieczeń.

Blazor WebAssembly Hostowana aplikacja może dostosować opublikowane pliki i pakowanie bibliotek DLL aplikacji przy użyciu następujących funkcji:

  • Inicjatory języka JavaScript, które umożliwiają dostosowywanie procesu rozruchu Blazor .
  • Rozszerzalność programu MSBuild w celu przekształcenia listy opublikowanych plików i zdefiniowania Blazor rozszerzeń publikowania. Blazor Rozszerzenia publikowania to pliki zdefiniowane podczas procesu publikowania, które stanowią alternatywną reprezentację zestawu plików wymaganych do uruchomienia opublikowanej Blazor WebAssembly aplikacji. W tym artykule utworzono rozszerzenie publikowania, Blazor które tworzy pakiet wieloczęściowy ze wszystkimi bibliotekami DLL aplikacji zapakowanymi w jeden plik, dzięki czemu biblioteki DLL można pobrać razem.

Podejście przedstawione w tym artykule służy jako punkt wyjścia dla deweloperów do opracowania własnych strategii i niestandardowych procesów ładowania.

Ostrzeżenie

Każde podejście podjęte w celu obejścia ograniczenia bezpieczeństwa musi być starannie przemyślane pod kątem jego skutków dla bezpieczeństwa. Zalecamy dalsze zapoznanie się z tematem u specjalistów ds. zabezpieczeń sieci w organizacji przed przyjęciem podejścia w tym artykule. Alternatywy do rozważenia:

  • Włącz urządzenia zabezpieczające i oprogramowanie zabezpieczające, aby umożliwić klientom sieciowym pobieranie i używanie dokładnych plików wymaganych przez aplikację Blazor WebAssembly .
  • Przełącz się z modelu hostingu Blazor WebAssembly do modelu hostinguBlazor Server, który obsługuje cały kod języka C# aplikacji na serwerze i nie wymaga pobierania bibliotek DLL do klientów. Blazor Server Oferuje również możliwość zachowania prywatności kodu w języku C# bez konieczności używania internetowych aplikacji interfejsu API na potrzeby prywatności kodu w języku C# w Blazor WebAssembly aplikacjach.

Eksperymentalny pakiet NuGet i przykładowa aplikacja

Podejście opisane w tym artykule jest używane przez pakiet eksperymentalnyMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) dla aplikacji przeznaczonych dla platformy .NET 6 lub nowszej. Pakiet zawiera elementy docelowe programu MSBuild, aby dostosować Blazor dane wyjściowe publikowania i inicjator języka JavaScript do używania niestandardowego modułu ładującego zasoby rozruchowe, z których każdy został szczegółowo opisany w dalszej części tego artykułu.

Kod eksperymentalny (zawiera źródło referencyjne pakietu NuGet i CustomPackagedApp przykładową aplikację)

Ostrzeżenie

Funkcje eksperymentalne i w wersji zapoznawczej są udostępniane na potrzeby zbierania opinii i nie są obsługiwane w środowisku produkcyjnym.

W dalszej części tego artykułu sekcja Dostosowywanie Blazor WebAssembly procesu ładowania za pośrednictwem pakietu NuGet wraz z trzema podsekcjami zawiera szczegółowe wyjaśnienia dotyczące konfiguracji i kodu w Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle pakiecie. Szczegółowe wyjaśnienia są ważne, aby zrozumieć, kiedy tworzysz własną strategię i niestandardowy proces ładowania aplikacji Blazor WebAssembly . Aby użyć opublikowanego, eksperymentalnego, nieobsługiwanego pakietu NuGet bez dostosowywania jako pokazu lokalnego, wykonaj następujące kroki:

  1. Użyj istniejącego rozwiązania hostowanego Blazor WebAssemblylub utwórz nowe rozwiązanie na podstawie Blazor WebAssembly szablonu projektu przy użyciu programu Visual Studio lub przekazując -ho|--hosted opcję do dotnet new polecenia (dotnet new blazorwasm -ho). Aby uzyskać więcej informacji, zobacz Tooling for ASP.NET Core Blazor.

  2. W projekcie Client dodaj pakiet eksperymentalny Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle .

    Uwaga

    Aby uzyskać instrukcje dodawania pakietów do aplikacji .NET, zobacz artykuły w sekcji Instalowanie pakietów i zarządzanie nimi w temacie Przepływ pracy użycia pakietów (dokumentacja programu NuGet). Sprawdź prawidłowe wersje pakietów pod adresem NuGet.org.

  3. W projekcie Server dodaj punkt końcowy służący do obsługi pliku pakietu (app.bundle). Przykładowy kod można znaleźć w sekcji Obsługa pakietu w sekcji aplikacji serwera hosta w tym artykule.

  4. Opublikuj aplikację w konfiguracji wydania.

Blazor WebAssembly Dostosowywanie procesu ładowania za pomocą pakietu NuGet

Ostrzeżenie

Wskazówki zawarte w tej sekcji z trzema podsekcjami dotyczą tworzenia pakietu NuGet od podstaw w celu zaimplementowania własnej strategii i niestandardowego procesu ładowania. Pakiet eksperymentalnyMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) dla platformy .NET 6 i 7 jest oparty na wskazówkach w tej sekcji. W przypadku korzystania z podanego pakietu w lokalnym pokazie podejścia do pobierania pakietów wieloczęściowych nie trzeba postępować zgodnie ze wskazówkami w tej sekcji. Aby uzyskać wskazówki dotyczące korzystania z podanego pakietu, zobacz sekcję Eksperymentalny pakiet NuGet i przykładową aplikację .

Blazorzasoby aplikacji są pakowane do pliku pakietu wieloczęściowego i ładowane przez przeglądarkę za pośrednictwem niestandardowego inicjatora języka JavaScript (JS). W przypadku aplikacji korzystającej z pakietu za pomocą JS inicjatora aplikacja wymaga tylko, aby plik pakietu był obsługiwany po żądaniu. Wszystkie inne aspekty tego podejścia są obsługiwane w sposób niewidoczny.

Do sposobu ładowania domyślnej opublikowanej Blazor aplikacji są wymagane cztery dostosowania:

  • Zadanie MSBuild w celu przekształcenia plików publikowania.
  • Pakiet NuGet z elementami docelowymi programu MSBuild, który łączy się w Blazor proces publikowania, przekształca dane wyjściowe i definiuje co najmniej jeden Blazor plik rozszerzenia publikowania (w tym przypadku pojedynczy pakiet).
  • Inicjator JS umożliwiający zaktualizowanie wywołania zwrotnego modułu Blazor WebAssembly ładującego zasobów w celu załadowania pakietu i zapewnienia aplikacji poszczególnym plikom.
  • Pomocnik w aplikacji hosta Server , aby upewnić się, że pakiet jest obsługiwany dla klientów na żądanie.

Tworzenie zadania MSBuild w celu dostosowania listy opublikowanych plików i zdefiniowania nowych rozszerzeń

Utwórz zadanie MSBuild jako publiczną klasę języka C#, którą można zaimportować w ramach kompilacji programu MSBuild i które mogą współdziałać z kompilacją.

Dla klasy C# wymagane są następujące elementy:

Uwaga

Pakiet NuGet dla przykładów w tym artykule nosi nazwę po pakiecie dostarczonym przez firmę Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Aby uzyskać wskazówki dotyczące nazewnictwa i tworzenia własnego pakietu NuGet, zobacz następujące artykuły 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>

Określ najnowsze wersje pakietów dla {VERSION} symboli zastępczych w NuGet.org:

Aby utworzyć zadanie MSBuild, utwórz publiczną klasę języka C# rozszerzającą Microsoft.Build.Utilities.Task (a nie System.Threading.Tasks.Task) i zadeklaruj trzy właściwości:

  • PublishBlazorBootStaticWebAsset: lista plików do opublikowania Blazor dla aplikacji.
  • BundlePath: ścieżka, w której jest zapisywany pakiet.
  • Extension: nowe rozszerzenia publikowania do uwzględnienia w kompilacji.

Poniższa przykładowa BundleBlazorAssets klasa to punkt wyjścia do dalszego dostosowywania:

  • W metodzie Execute pakiet jest tworzony na podstawie następujących trzech typów plików:
    • Pliki JavaScript (dotnet.js)
    • Pliki WASM (dotnet.wasm)
    • Biblioteki DLL aplikacji (.dll)
  • Zostanie multipart/form-data utworzony pakiet. Każdy plik jest dodawany do pakietu z odpowiednimi opisami za pośrednictwem nagłówka Content-Disposition i nagłówka Content-Type.
  • Po utworzeniu pakietu pakiet jest zapisywany w pliku.
  • Kompilacja jest skonfigurowana dla rozszerzenia. Poniższy kod tworzy element rozszerzenia i dodaje go do Extension właściwości . Każdy element rozszerzenia zawiera trzy fragmenty danych:
    • Ścieżka do pliku rozszerzenia.
    • Ścieżka adresu URL względem katalogu głównego Blazor WebAssembly aplikacji.
    • Nazwa rozszerzenia, które grupuje pliki utworzone przez określone rozszerzenie.

Po osiągnięciu powyższych celów zadanie MSBuild jest tworzone do dostosowywania danych wyjściowych publikowania Blazor . Blazor dba o gromadzenie rozszerzeń i upewniając się, że rozszerzenia są kopiowane do poprawnej lokalizacji w folderze danych wyjściowych publikowania (na przykład bin\Release\net6.0\publish). Te same optymalizacje (na przykład kompresja) są stosowane do plików JavaScript, WASM i DLL, które Blazor mają zastosowanie do innych plików.

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

Tworzenie pakietu NuGet w celu automatycznego przekształcania danych wyjściowych publikowania

Wygeneruj pakiet NuGet z miejscami docelowymi programu MSBuild, które są automatycznie dołączane po odwołaniu do pakietu:

  • Utwórz nowy Razor projekt biblioteki klas (RCL).
  • Utwórz plik docelowy zgodnie z konwencjami NuGet, aby automatycznie importować pakiet w projektach korzystających z. Na przykład utwórz build\net6.0\{PACKAGE ID}.targetselement , gdzie {PACKAGE ID} jest identyfikatorem pakietu.
  • Zbierz dane wyjściowe z biblioteki klas zawierającej zadanie MSBuild i upewnij się, że dane wyjściowe są pakowane we właściwej lokalizacji.
  • Dodaj niezbędny kod MSBuild do dołączenia do potoku Blazor i wywołaj zadanie MSBuild w celu wygenerowania pakietu.

Podejście opisane w tej sekcji używa tylko pakietu do dostarczania elementów docelowych i zawartości, która różni się od większości pakietów, w których pakiet zawiera bibliotekę DLL.

Ostrzeżenie

Przykładowy pakiet opisany w tej sekcji przedstawia sposób dostosowywania procesu publikowania Blazor . Przykładowy pakiet NuGet jest używany tylko jako lokalny pokaz. Używanie tego pakietu w środowisku produkcyjnym nie jest obsługiwane.

Uwaga

Pakiet NuGet dla przykładów w tym artykule nosi nazwę po pakiecie dostarczonym przez firmę Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Aby uzyskać wskazówki dotyczące nazewnictwa i tworzenia własnego pakietu NuGet, zobacz następujące artykuły 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>

Uwaga

Właściwość <NoWarn>NU5100</NoWarn> w poprzednim przykładzie pomija ostrzeżenie dotyczące zestawów umieszczonych w folderze tasks . Aby uzyskać więcej informacji, zobacz Ostrzeżenie narzędzia NuGet NU5100.

Dodaj plik, .targets aby połączyć zadanie MSBuild z potokiem kompilacji. W tym pliku są realizowane następujące cele:

  • Zaimportuj zadanie do procesu kompilacji. Należy pamiętać, że ścieżka do biblioteki DLL jest względna względem ostatecznej lokalizacji pliku w pakiecie.
  • Właściwość ComputeBlazorExtensionsDependsOn dołącza obiekt docelowy niestandardowy do potoku Blazor WebAssembly .
  • Extension Przechwyć właściwość w danych wyjściowych zadania i dodaj ją, aby poinformować BlazorPublishExtensionBlazor o rozszerzeniu. Wywołanie zadania w obiekcie docelowym powoduje wygenerowanie pakietu. Lista opublikowanych plików jest dostarczana przez Blazor WebAssembly potok w PublishBlazorBootStaticWebAsset grupie elementów. Ścieżka pakietu jest definiowana IntermediateOutputPath przy użyciu elementu (zazwyczaj wewnątrz obj folderu). Ostatecznie pakiet jest kopiowany automatycznie do prawidłowej lokalizacji w folderze danych wyjściowych publikowania (na przykład bin\Release\net6.0\publish).

Po odwołyniu się do pakietu generuje pakiet Blazor plików podczas publikowania.

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>

Automatyczne uruchamianie Blazor z pakietu

Pakiet NuGet wykorzystuje inicjatory języka JavaScript (JS), aby automatycznie uruchamiać aplikację Blazor WebAssembly z pakietu zamiast używać pojedynczych plików DLL. JSInicjatory są używane do zmieniania modułu ładującego zasób rozruchowy Blazori używania pakietu.

Aby utworzyć JS inicjator, dodaj JS plik o nazwie {NAME}.lib.module.js do wwwroot folderu projektu pakietu, gdzie {NAME} symbol zastępczy jest identyfikatorem pakietu. Na przykład plik pakietu firmy Microsoft ma nazwę Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Wyeksportowane funkcje beforeWebAssemblyStart i afterWebAssemblyStarted obsługa ładowania.

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

Aby utworzyć JS inicjator, dodaj JS plik o nazwie {NAME}.lib.module.js do wwwroot folderu projektu pakietu, gdzie {NAME} symbol zastępczy jest identyfikatorem pakietu. Na przykład plik pakietu firmy Microsoft ma nazwę Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Wyeksportowane funkcje beforeStart i afterStarted obsługa ładowania.

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

Obsługa pakietu z aplikacji serwera hosta

Ze względu na ograniczenia zabezpieczeń ASP.NET Core domyślnie nie obsługuje app.bundle pliku. Pomocnik przetwarzania żądań jest wymagany do obsługi pliku, gdy jest on żądany przez klientów.

Uwaga

Ponieważ te same optymalizacje są niewidocznie stosowane do rozszerzeń publikowania zastosowanych do plików aplikacji, app.bundle.gz pliki zasobów i app.bundle.br skompresowane są generowane automatycznie podczas publikowania.

Umieść kod C# w Program.csServer projekcie bezpośrednio przed wierszem, który ustawia plik rezerwowy na index.html (app.MapFallbackToFile("index.html");), aby odpowiedzieć na żądanie dla pliku pakietu (na przykład 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);
});

Typ zawartości jest zgodny z typem zdefiniowanym wcześniej w zadaniu kompilacji. Punkt końcowy sprawdza kodowanie zawartości akceptowane przez przeglądarkę i obsługuje optymalny plik, Brotli (.br) lub Gzip (.gz).