ASP.NET Core Blazor Hybrid static files

Note

This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 8 version of this article.

This article describes how to consume static asset files in Blazor Hybrid apps.

In a Blazor Hybrid app, static files are app resources, accessed by Razor components using the following approaches:

When static assets are only used in the Razor components, static assets can be consumed from the web root (wwwroot folder) in a similar way to Blazor WebAssembly and Blazor Server apps. For more information, see the Static assets limited to Razor components section.

.NET MAUI

In .NET MAUI apps, raw assets using the MauiAsset build action and .NET MAUI file system helpers are used for static assets.

Note

Interfaces, classes, and supporting types to work with storage on devices across all supported platforms for features such as choosing a file, saving preferences, and using secure storage are in the Microsoft.Maui.Storage namespace. The namespace is available throughout a MAUI Blazor Hybrid app, so there's no need to specify a using statement in a class file or an @using Razor directive in a Razor component for the namespace.

Place raw assets into the Resources/Raw folder of the app. The example in this section uses a static text file.

Resources/Raw/Data.txt:

This is text from a static text file resource.

The following Razor component:

Pages/StaticAssetExample.razor:

@page "/static-asset-example"
@using System.IO
@using Microsoft.Extensions.Logging
@inject ILogger<StaticAssetExample> Logger

<h1>Static Asset Example</h1>

<p>@dataResourceText</p>

@code {
    public string dataResourceText = "Loading resource ...";

    protected override async Task OnInitializedAsync()
    {
        try
        {
            using var stream = 
                await FileSystem.OpenAppPackageFileAsync("Data.txt");
            using var reader = new StreamReader(stream);

            dataResourceText = await reader.ReadToEndAsync();
        }
        catch (FileNotFoundException ex)
        {
            dataResourceText = "Data file not found.";
            Logger.LogError(ex, "'Resource/Raw/Data.txt' not found.");
        }
    }
}

For more information, see the following resources:

WPF

Place the asset into a folder of the app, typically at the project's root, such as a Resources folder. The example in this section uses a static text file.

Resources/Data.txt:

This is text from a static text file resource.

If a Properties folder doesn't exist in the app, create a Properties folder in the root of the app.

If the Properties folder doesn't contain a resources file (Resources.resx), create the file in Solution Explorer with the Add > New Item contextual menu command.

Double-click the Resource.resx file.

Select Strings > Files from the dropdown list.

Select Add Resource > Add Existing File. If prompted by Visual Studio to confirm editing the file, select Yes. Navigate to the Resources folder, select the Data.txt file, and select Open.

In the following example component, ResourceManager.GetString obtains the string resource's text for display.

Warning

Never use ResourceManager methods with untrusted data.

StaticAssetExample.razor:

@page "/static-asset-example"
@using System.Resources

<h1>Static Asset Example</h1>

<p>@dataResourceText</p>

@code {
    public string dataResourceText = "Loading resource ...";

    protected override void OnInitialized()
    {
        var resources = 
            new ResourceManager(typeof(WpfBlazor.Properties.Resources));

        dataResourceText = resources.GetString("Data") ?? "'Data' not found.";
    }
}

Windows Forms

Place the asset into a folder of the app, typically at the project's root, such as a Resources folder. The example in this section uses a static text file.

Resources/Data.txt:

This is text from a static text file resource.

Examine the files associated with Form1 in Solution Explorer. If Form1 doesn't have a resource file (.resx), add a Form1.resx file with the Add > New Item contextual menu command.

Double-click the Form1.resx file.

Select Strings > Files from the dropdown list.

Select Add Resource > Add Existing File. If prompted by Visual Studio to confirm editing the file, select Yes. Navigate to the Resources folder, select the Data.txt file, and select Open.

In the following example component:

  • The app's assembly name is WinFormsBlazor. The ResourceManager's base name is set to the assembly name of Form1 ( WinFormsBlazor.Form1).
  • ResourceManager.GetString obtains the string resource's text for display.

Warning

Never use ResourceManager methods with untrusted data.

StaticAssetExample.razor:

@page "/static-asset-example"
@using System.Resources

<h1>Static Asset Example</h1>

<p>@dataResourceText</p>

@code {
    public string dataResourceText = "Loading resource ...";

    protected override async Task OnInitializedAsync()
    {   
        var resources = 
            new ResourceManager("WinFormsBlazor.Form1", this.GetType().Assembly);

        dataResourceText = resources.GetString("Data") ?? "'Data' not found.";
    }
}

Static assets limited to Razor components

A BlazorWebView control has a configured host file (HostPage), typically wwwroot/index.html. The HostPage path is relative to the project. All static web assets (scripts, CSS files, images, and other files) that are referenced from a BlazorWebView are relative to its configured HostPage.

Static web assets from a Razor class library (RCL) use special paths: _content/{PACKAGE ID}/{PATH AND FILE NAME}. The {PACKAGE ID} placeholder is the library's package ID. The package ID defaults to the project's assembly name if <PackageId> isn't specified in the project file. The {PATH AND FILE NAME} placeholder is path and file name under wwwroot. These paths are logically subpaths of the app's wwwroot folder, although they're actually coming from other packages or projects. Component-specific CSS style bundles are also built at the root of the wwwroot folder.

The web root of the HostPage determines which subset of static assets are available:

  • wwwroot/index.html (recommended): All assets in the app's wwwroot folder are available (for example: wwwroot/image.png is available from /image.png), including subfolders (for example: wwwroot/subfolder/image.png is available from /subfolder/image.png). RCL static assets in the RCL's wwwroot folder are available (for example: wwwroot/image.png is available from the path _content/{PACKAGE ID}/image.png), including subfolders (for example: wwwroot/subfolder/image.png is available from the path _content/{PACKAGE ID}/subfolder/image.png).
  • wwwroot/{PATH}/index.html: All assets in the app's wwwroot/{PATH} folder are available using app web root relative paths. RCL static assets in wwwroot/{PATH} are not available because they would be in a non-existent theoretical location, such as ../../_content/{PACKAGE ID}/{PATH}, which is not a supported relative path.
  • wwwroot/_content/{PACKAGE ID}/index.html: All assets in the RCL's wwwroot/{PATH} folder are available using RCL web root relative paths. The app's static assets in wwwroot/{PATH} are not available because they would be in a non-existent theoretical location, such as ../../{PATH}, which is not a supported relative path.

For most apps, we recommend placing the HostPage at the root of the wwwroot folder of the app, which provides the greatest flexibility for supplying static assets from the app, RCLs, and via subfolders of the app and RCLs.

The following examples demonstrate referencing static assets from the app's web root (wwwroot folder) with a HostPage rooted in the wwwroot folder.

wwwroot/data.txt:

This is text from a static text file resource.

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

The following Jeep® image is also used in this section's example. You can right-click the following image to save it locally for use in a local test app.

wwwroot/jeep-yj.png:

Jeep YJ®

In a Razor component:

StaticAssetExample2.razor:

@page "/static-asset-example-2"
@using Microsoft.Extensions.Logging
@implements IAsyncDisposable
@inject IJSRuntime JS
@inject ILogger<StaticAssetExample2> Logger

<h1>Static Asset Example 2</h1>

<h2>Read a file</h2>

<p>@dataResourceText</p>

<h2>Call JavaScript</h2>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>@result</p>

<h2>Show an image</h2>

<p><img alt="1991 Jeep YJ" src="/jeep-yj.png" /></p>

<p>
    <em>Jeep</em> and <em>Jeep YJ</em> are registered trademarks of 
    <a href="https://www.stellantis.com">FCA US LLC (Stellantis NV)</a>.
</p>

@code {
    private string dataResourceText = "Loading resource ...";
    private IJSObjectReference? module;
    private string result;

    protected override async Task OnInitializedAsync()
    {   
        try
        {
            dataResourceText = await ReadData();
        }
        catch (FileNotFoundException ex)
        {
            dataResourceText = "Data file not found.";
            Logger.LogError(ex, "'wwwroot/data.txt' not found.");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message) =>
        module is not null ?
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

In .NET MAUI apps, add the following ReadData method to the @code block of the preceding component:

private async Task<string> ReadData()
{
    using var stream = await FileSystem.OpenAppPackageFileAsync("wwwroot/data.txt");
    using var reader = new StreamReader(stream);

    return await reader.ReadToEndAsync();
}

In WPF and Windows Forms apps, add the following ReadData method to the @code block of the preceding component:

private async Task<string> ReadData()
{
    using var reader = new StreamReader("wwwroot/data.txt");

    return await reader.ReadToEndAsync();
}

Collocated JavaScript files are also accessible at logical subpaths of wwwroot. Instead of using the script described earlier for the showPrompt function in wwwroot/scripts.js, the following collocated JavaScript file for the StaticAssetExample2 component also makes the function available.

Pages/StaticAssetExample2.razor.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

Modify the module object reference in the StaticAssetExample2 component to use the collocated JavaScript file path (./Pages/StaticAssetExample2.razor.js):

module = await JS.InvokeAsync<IJSObjectReference>("import", 
    "./Pages/StaticAssetExample2.razor.js");

Trademarks

Jeep and Jeep YJ are registered trademarks of FCA US LLC (Stellantis NV).

Additional resources