Custom deployment layout for Blazor WebAssembly apps

Javier Calvarro Nelson

Some environments block the download and execution of DLLs from the network to prevent the potential spread of malware, which can also block downloading Blazor WebAssembly apps. To enable Blazor WebAssembly in these environments, we introduced in .NET 6 new extensibility points that allows developers to customize the published files and packaging of Blazor WebAssembly apps. These customizations can then be packaged as a reusable NuGet package.

There are two main features that make this possible:

  • JavaScript initializers that allow customizing the Blazor boot process.
  • MSBuild extensibility to transform the list of publish files and define Blazor publish extensions.

JavaScript initializers

JavaScript initializers are JavaScript modules loaded during the Blazor boot process. These modules can export two functions that get called at specific points early in the lifecycle of the host app:

  • beforeStart: Invoked by Blazor before the app is started.
  • afterStarted: Invoked by Blazor after the .NET runtime has started.

In Blazor WebAssembly apps, beforeStarts receives two pieces of data:

  • Blazor WebAssembly options that can be changed to provide a custom resource loader.
  • An extensions object that contains a collection of extensions defined for the app. Each of these extensions is an JavaScript object that contains a list of files relevant to that extension.

Blazor publish extensions

Blazor publish extensions are files that can be defined as part of the publish process and that provide an alternative representation for the set of files needed to run the published app.

For example, in this post we’ll create a Blazor publish extension that produces a multipart bundle with all the app DLLs packed into a single file so they can be downloaded together. We hope this sample will serve as a starting point for people to come up with their own strategies and custom loading processes.

Customizing the Blazor WebAssembly loading process via a NuGet package

In this example, we’re going to pack all the Blazor app resources into a bundle file as a multipart file bundle and load it on the browser via a custom JavaScript initializer. For an app consuming this package, they only need to make sure that the bundle file is being served. Everything else is handled transparently.

There are four things that we need to customize how a published Blazor app loads:

  • An MSBuild task to transform the publish files.
  • A package with MSBuild targets that hooks into the Blazor publishing process, transforms the output, and defines one or more Blazor publish extension files (in this case, a single bundle).
  • A JavaScript initializer to update the Blazor WebAssembly resource loader callback so that it loads the bundle and provides the app with the individual files.
  • A small helper on the host server app to ensure we serve the bundle.

Writing an MSBuild task to customize the list of published files and define new extensions

An MSBuild task is a public C# class that can be imported as part of an MSBuild compilation and that can interact with the build.

Before we write our C# class, we need to do the following:

  • We need to create a new class library project.
  • Change the target framework to netstandard2.0
  • Reference the MSBuild packages

After that, the csproj file should look something like this:

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

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

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Framework" Version="16.10.0" />
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10.0" />
  </ItemGroup>

</Project>

Now that our project is created, we can create our MSBuild task. To do so, we create a public class extending Microsoft.Build.Utilities.Task (not System.Threading.Tasks.Task) and declare three properties:

  • PublishBlazorBootStaticWebAsset: The list of files to publish for the Blazor app
  • BundlePath: The path where we need to write the bundle.
  • Extension: The new publish extensions to include in the build.

A sketch of the code can be seen below.

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()
        {
          ...
        }
    }
}

The remaining piece is to implement the Execute method, where we take the files and create the bundle. There are three types of files we are going to deal with:

  • JavaScript files (dotnet.js)
  • WASM files (dotnet.wasm)
  • App DLLs.

We are going to create a multipart/form-data bundle and add each file to the bundle with their respective descriptions via the content disposition header and the content type header. The code can be seen below:

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

Now that we’ve created our bundle, we need to write it to a file:

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

Finally, we need to let the build know about our extension. We do so by creating an extension item and adding it to the Extension property. Each extension item contains three pieces of data:

  • The path to the extension file
  • The URL path relative to the root of the Blazor WebAssembly app.
  • The name of the extension, which groups the files produced by a given extension. We’ll use this to refer to the extension later.

In our extension we define the item as follows:

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

Extension = new ITaskItem[] { bundleItem };

return true;

With that, we’ve authored an MSBuild task for customizing the Blazor publish output. Blazor will take care of gathering the extensions and making sure that they get copied to the right place in the publish output folder and will apply the same optimizations (compression) it applies to other files.

For clarity, here is the full class in one snippet:

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

Now that we have an MSBuild task capable of transforming the publish output, we need a bit of plumbing code to hook it to the MSBuild pipeline.

Authoring a NuGet package for automatically transforming the publish output

A good way to create a reusable solution is to generate a NuGet package with MSBuild targets that are automatically included when the package is referenced. For that, the steps are:

  • Create a new Razor class library project.
  • Create a targets file following the NuGet conventions to automatically import it in consuming projects.
  • Collect the output from the class library containing the MSBuild task and make sure it gets packed in the right location.
  • Make sure all the required files are packed in the right location.
  • Add the necessary MSBuild code to attach to the Blazor pipeline and invoke our task to generate the bundle.

First we create a Razor class library and remove all the content from it.

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

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>

Next we create a file build\net6.0\<<PackageId>>.targets where <<PackageId>> is the name of our package. We’ll fill in the contents later.

After that, we need to make sure we collect all the DLLs required for the MSBuild task. We can do so by creating a custom target in the package project file. In our target we invoke MSBuild over the project with our task and capture the output in the _TasksProjectOutputs item group as follows:

<Target Name="GetTasksOutputDlls" BeforeTargets="CoreCompile">
  <MSBuild Projects="$(PathToTasksFolder)" Targets="Publish;PublishItemsOutputGroup" Properties="Configuration=Release">
    <Output TaskParameter="TargetOutputs" ItemName="_TasksProjectOutputs" />
  </MSBuild>
</Target>

The next step is to make sure that our content is included in the package. To include the targets file we use the following snippet:

<ItemGroup>
  <None Update="build\**" Pack="true" PackagePath="%(Identity)" />
</ItemGroup>

This tells NuGet to pack the file and place it on the same path in the package.

We add the task DLLs as content after we’ve invoked the MSBuild task inside the tasks subfolder.

<Target Name="GetTasksOutputDlls" BeforeTargets="CoreCompile">
  ...
  <ItemGroup>
    <Content Include="@(_TasksProjectOutputs)" Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'" Pack="true" PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)" KeepMetadata="Pack;PackagePath" />
  </ItemGroup>
</Target>

Finally, we need to setup some properties to keep NuGet happy, since this doesn’t include a library DLL like most packages do (we are only using it as a mechanism to deliver our targets and content).

The finished project file is displayed below:

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

  <PropertyGroup>
    <!-- Suppress the warning about the assemblies we are putting in the task folder. -->
    <NoWarn>NU5100</NoWarn>
    <TargetFramework>net6.0</TargetFramework>
    <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>

All that remains now is to add a .targets file to wire up our task to the build pipeline. In this file we need to do the following:

  • Import our task into the build process.
  • Attach a custom target to the Blazor WebAssembly build pipeline.
  • Invoke our task in the target to produce the results.

We start by defining an empty project on the file

<Project>
</Project>

Next, we import our task. Note that the path to the DLL is relative to where this file will be in the package:

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

We define a target that invokes our bundling task:

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

The list of published files is provided by the Blazor WebAssembly pipeline in the PublishBlazorBootStaticWebAsset item group.

We define the bundle path using the IntermediateOutputPath (typically inside the obj folder). The bundle will later get copied automatically to the right location in the publish output folder.

Finally, we capture the Extension property on the task output and add it to BlazorPublishExtension to tell Blazor about the extension.

We can now attach our custom target to the Blazor WebAssembly pipeline:

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

With this, we have a package that when referenced will generate a bundle of the Blazor files during publish. However, we haven’t yet seen how to automatically bootstrap a Blazor WebAssembly app from that bundle instead of using the DLLs. We’ll tackle that next.

Automatically bootstrap Blazor from the bundle

This is where we will leverage JavaScript initializers. We’ll use a JavaScript initializer to change the Blazor boot resource loader and use our bundle instead.

To create a JavaScript initializer, add a JavaScript file with the name <<PackageId>>.lib.module.js to the wwwroot folder of the package project. Once we’ve done that, we can export two functions to handle the loading.

export async function beforeStart(wasmOptions, extensions) {
  ...
}

export async function afterStarted(blazor) {
  ...
}

The approach that we’re going to follow is:

  • Detect if our extension is available.
  • Download the bundle
  • Parse the contents and create a map of resources using object URLs.
  • Update the wasmOptions.loadBootResource with our own function that resolves the resources using object URLs.
  • After the app has started, revoke the object URLs to release memory.

Most of the steps happen inside beforeStart, with only the last happening in afterStarted.

To detect if our extension is available, we check the extensions argument:

if (!extensions || !extensions.multipart) {
    return;
}

Remember that multipart bit that we added in the extension definition inside the Task? It shows up here.

Next we’re going to download the bundle and parse its contents into a resources map. The resources map is defined locally at the top of the file.

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));
    }
} catch (error) {
    console.log(error);
}

After that, we customize the options to use our custom boot resource loader. We do this after we’ve created the object URLs:

wasmOptions.loadBootResource = function (type, name, defaultUri, integrity) {
    return resources.get(name) ?? null;
}

Finally, we release all the object URLs after the app has started within the afterStarted function:

for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
}

Here is all the code for handling the loading in one snippet:

const resources = new Map();

export async function beforeStart(wasmOptions, extensions) {
    // Simple way of detecting we are in web assembly
    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));
        }
        wasmOptions.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);
    }
}

Serving the bundle from the host server app

We have our app.bundle file (and app.bundle.gz and app.bundle.br, since we transparently apply the same optimizations to the extensions that we do for the app files) but ASP.NET Core doesn’t know how to serve it (and won’t do so by default for security reasons) so we need a small helper to make that happen. Thankfully, we can do this in a few lines of code using minimal APIs.

Add an endpoint for serving app.bundle in Program.cs:

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

Notice the content type matches the one we defined in the build task. The endpoint checks for the content encodings accepted by the browser and serves the most optimal file.

And that’s a wrap! We can now reference our NuGet package from an app, add a small helper to our server host and completely change how the Blazor WebAssembly app loads.

image

You can find the full code for this sample on GitHub.

Using the MultipartBundle package

A prebuilt version of the experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package is available on NuGet to try out with existing apps.

To try multipart bundling in an existing ASP.NET Core hosted Blazor WebAssembly app:

  1. Add a reference to the Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package from the Blazor WebAssembly client project.
  2. Update the server project to add the endpoint for serving the bundle.
  3. Publish the app.

If you’ve had issues with the download of Blazor WebAssembly apps getting blocked we’d love to know how well this approach works for you. Leave us a comment on the GitHub with your thoughts.

Thanks!

16 comments

Discussion is closed. Login to edit/delete existing comments.

  • Andrzej WÅ‚oszczyÅ„ski 0

    Is this solution smart enough to download the bundle only once and cache it in the browser? Will it download it again when new app version is published?

    Do you have any information on how effective this solution can be? Have you discussed the problem with firewall vendors?

  • Roberto Mencia Franco 0

    This seems like a very poor fix for a design flaw in MS blazor webassembly.

    The fact is that firewalls are blocking the application and blazor was not designed to deal with it from the beginning. All the people watching the demos and believe that this is so easy and then half of their customers can’t access the app and need to deal with lots of unhappy customers and your boss. Not good enough.

    Do we really need to follow this super long post and custom code to achieve what was is supposed to be something that should just work out of the box?

    Maybe you can add this already in the blazor template from the beginning. Also add a button in VS to add the required patch to existing projects.
    And maybe later re-design blazor to work the right way to avoid the issue all together.

    Also, get the solution in a way that out of the box when building in the azuredevops builds.

    • Daniel RothMicrosoft employee 1

      Hi Roberto. We do aspire to provide an out of the box solution for Blazor WebAssembly apps that works in all environments, but everyone’s environment is a bit different and we’re not sure yet what solution would work for everyone. So as a first step, we’ve added these extensibility features in .NET 6 that enable customizing the app packaging, which is what this blog post covers. This should enable Blazor users who are hitting these environmental problems to unblock themselves in their specific environments. We’re also providing the multipart bundle experimental package as a potential general purpose packaging solution, and we’re looking for community feedback on whether whether it works in existing problematic environments. If we get positive feedback that this is a successful solution, we’ll consider adding it directly to the framework. If you have additional ideas on how we could package Blazor WebAssembly apps to work in these more restrictive environments, please suggest them on GitHub and we’ll consider them too.

      • Roberto Mencia Franco 0

        Thanks for the explanation, it makes sense.

        The whole thing for me is that we are a small team of 2 and there are lots of small teams of 1-3 that can’t afford having a DevOps guy dealing with deployment issues and complex builds and we don’t have too much time to get into some complicated solutions that may be required for large organisations.

        My point is that it would be greatly appreciated that there was an out of the box solution that works by adding a simple nuget package (or similar) and doesn’t do any of the gymnastics needed by large and complex scenarios. Something that just works. Simple build locally and in the cloud and deploy using Azure Devops. If something complex is needed, it is because that complex scenario comes in an organisation with 20-30 developers and they can dedicate 1-2 weeks from one guru to implement that specific need, and maintain it. Most of us, we don’t have the resources or time.

        Also, most people don’t need that. We need something simple that works out-of-the-box, great and simple as all the other MS tools work (we are a bit spoiled here).

        I really love Blazor because I don’t need to have to deal with all these painful packaging steps to build JS Angular, Nodejs and other JS frameworks that most of us don’t care about and we’ve chosen Blazor. Blazor is simple and just works.

        It’s all about making it simple also for newcomers to dotnet. Blazor is new and not many people have experience with it. I’m still learning and I don’t need to make that more difficult. If we need to deal with added complexities, it just complicates everything and slows down adoption.

        You’ve added great new features to allow adding Blazor components to Angular applications, but remember, these applications now don’t work anymore because the proxies are blocking the new Blazor components. The JS guys are going to freak out when half of their customers start getting errors.
        However, if the default Blazor webassemly setup packages everything needed to deliver the application to the customers bypassing the proxy issues, everyone will be happy. At least there is something that works and can be improved later if needed.

        That is my point. Getting the basics simple for adoption.

        • Javier Calvarro NelsonMicrosoft employee 1

          Thanks Roberto,

          As Dan mentioned here, our main concern is that we don’t control the heuristics used by different vendors to decide what works and what doesn’t, and there are no guarantees that it won’t change. At the same time, this is not a problem that affects a majority of users (that we know of) and so doing something out of the box imposes a potential performance penalty on all other users.

          It is for that reason that we built the solution in a way that can be added to an existing application through a NuGet package. As a consumer, you add a reference to the NuGet package, register an endpoint to serve the new files and that’s all you have to do on your part.

          The blog post goes into all the technical details on how to achieve it, however at the core, it comes down to implementing a class, using JS to initialize the app and a helper on the server to send the files. The rest is plumbing.

          We hope folks in the community that are running into this issue share their solutions with others in the form of NuGet packages (similar to the one we created) in a way that everyone can benefit them.

          Hope this helps

          • Roberto Mencia Franco 0

            How would that work with static web sites? In this scenario we can’t really serve the files because our web site is not running code to serve any DLLs.

    • Peter N. Moore 0

      I’m not happy about this issue either – no one is – but calling it a “design flaw” is nonsense. One never knows what firewalls are going to block. They could easily start blocking WASM files at some point (in fact I’d be surprised if many aren’t already). Our current production app is an Angular SPA and I’ve had firewalls block it just because they didn’t like that there were too many “suspicious” API calls. These things are black boxes, like spam filters. You get a paranoid (maybe for good reason) person in charge of infosec who cranks it up to the max and all bets are off.

      If you’re looking to make the next Facebook or some other billion-user consumer app then you’re barking up the wrong tree with Blazor to begin with. If you’re making an enterprise or LOB app which is what it’s more suited to then you should know your customers and be in a position to manage these kinds of customer-relations issues. Or use Blazor server-side which is less susceptible to this. (Although firewalls can always decide to block websockets too).

  • Matt 0

    I was thinking exactly the same as Roberto as I read this, would be good to have a handle on how many users this might affect, although of course that’s not a simple question to answer. Presumably for consumer users it’s a virtual non-issue, internal business users (in theory) we can discuss/resolve with IT, other b2b users who knows?

    It would of course be great to have an option somewhere to turn this behaviour on!

    • Daniel RothMicrosoft employee 0

      Hi Matt. We think this only impacts a small subset of Blazor WebAssembly users, not the majority, but enough users reported the problem that we’re exploring ways to address it. You’re correct that sometimes users can work with their IT departments to remove the restriction for their apps. When this isn’t feasible, users can repackage the app with different files name extensions, which we provide instructions on how to do. In other cases the inspection of the files is deeper, so we added this extensibility in .NET 6 to enable further customization of the app packaging. These extensibility points give users the flexibility to adapt to the requirements of specific environments. We’re also exploring multipart bundling as a generic solution, and we’re looking for verification from users that are currently blocked that this approach works for them. The Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package is available to tryout today, so if you’re hitting this issue in your environment, give it a try and let us know if it works for you.

      • Matt 0

        Fair enough, I read through the post again and this doesn’t seem to bad should it be needed, good to see these discussions happening as well.

        Presumably frontend monitoring tools like Sentry would in theory be capable of picking up on this kind of issue, during the initial bootstrap of the app? Users obviously don’t (or can’t) always tell you when they’ve been unable to start your app.

        I’ve used blazor in a small project so far and I’m definitely a fan, considering it for a larger project currently.

  • Paul Guz 0

    Thanks for coming up with a solution.

    We have a customer who has come across this problem. There may be more users than you think who face this – you had a github issue regarding it, asking for examples, but closed it very quickly.

  • Laughing John 1

    Thanks for this, it’s a really great start! And I’d like to echo Roberto’s point about an out of the box solution.

    Couple of points:
    1) A lot of the article above seems to be about creating a Nuget package. Am I correct in thinking we don’t have to do that part, we can just build the MSBuild task and reference the DLL without all of that pain?
    2) I think this problem is a bit more widespread than you seem to think. I appreciate it depends on the nature of the application, but I would have thought any public/client facing Blazor webassembly app could have this issue when run from a workplace. We develop a software solution and have a “private” Blazor application used by our clients, and we obviously have no control over their firewall policies, and being financial in nature they tend to understandably be very strict. I’m sure there’s a reason the Silverlight team came up with Xaps…! 🙂

    Anyway, thanks again, this will make a big difference!

    • Daniel RothMicrosoft employee 1

      Hi John. There’s a prebuilt experimental NuGet package that you can use to try out this functionality: Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. You then also need to configure your host to serve the published app.bundle file. No need to build the package yourself if you just want to try out the functionality. We walked through the steps of how the package was built in case some users need to customize publishing further. I’ve update the blog post to try and make this clearer.

      • Roberto Mencia Franco 0

        Hi Daniel,

        Have you discussed with the team to add a Task to Azure DevOps build Tasks that could be added after the build step is finished and you can pass some parameters to it to basically do the same as described in the post but during the build instead of having to implement it in the code?

        It could be easier to just prepare the result of the standard compilation and update the required files to enable the process.
        This way, by build configuration we can update/tweak the settings without having to make code changes, just change the config in the build an rebuild. Also, this task could create multiple versions of the App bundle including the default one we have now. Then, in case one of the bundles is blocked try another one with different encoding type and repeat until all the options are finished. As you mentioned earlier, maybe only very few cases will happen, but those are probably the ones that give us the most amount of problems also.

        Then, for tracking purposes, the JS Loader code in the page can call a service in the website to report any blocking activity so it can be addressed by the team.

        I’m happy to share the stats of failures with the MS team to find alternative solutions in case of blocked dlls after transformation.

        Also, this way we could also target Static WebAssembly sites and not only hosted.

        Thanks.

  • Dean Bullen 0

    Could I ask what the symptom is, when having this problem? I have a couple of customers getting the error below – is that what this is?

    Mixed Content: The page at '' was loaded over HTTPS, but requested an insecure resource ''.  This request has been blocked; the content must be served over HTTPS.
    Error: Failed to start platform. Reason:
    TypeError: Failed to fetch
      at blazor.webassembly.js:1
      at blazor.webassembly.js:1
      at Object.throw (blazor.webassembly.js:1)
      at s (blazor.webassembly.js:1)

    Note: The message actually contains ” exactly as shown – not any actual URL

    • Daniel RothMicrosoft employee 0

      Hi Dean. That sounds different. That sounds like something in the app tried to do a fetch request over HTTP even though the app was loaded over HTTPS, and as a result the request was blocked. The issues with DLLs getting blocked I believe usually manifests as failed requests for the DLLs.

Feedback usabilla icon