Share assets across web and native clients using a Razor class library (RCL)

Note

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

Warning

This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 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 9 version of this article.

Use a Razor class library (RCL) to share Razor components, C# code, and static assets across web and native client projects.

This article builds on the general concepts found in the following articles:

The examples in this article share assets between a server-side Blazor app and a .NET MAUI Blazor Hybrid app in the same solution:

  • Although a server-side Blazor app is used, the guidance applies equally to Blazor WebAssembly apps sharing assets with a Blazor Hybrid app.
  • Projects are in the same solution, but an RCL can supply shared assets to projects outside of a solution.
  • The RCL is added as a project to the solution, but any RCL can be published as a NuGet package. A NuGet package can supply shared assets to web and native client projects.
  • The order that the projects are created isn't important. However, projects that rely on an RCL for assets must create a project reference to the RCL after the RCL is created.

For guidance on creating an RCL, see Consume ASP.NET Core Razor components from a Razor class library (RCL). Optionally, access the additional guidance on RCLs that apply broadly to ASP.NET Core apps in Reusable Razor UI in class libraries with ASP.NET Core.

Target frameworks for ClickOnce deployments

To publish a WPF or Windows Forms project with a Razor class library (RCL) in .NET 6 with ClickOnce, the RCL must target net6.0-windows in addition to net6.0.

Example:

<TargetFrameworks>net6.0;net6.0-windows</TargetFrameworks>

For more information, see the following articles:

Share web UI Razor components, code, and static assets

Components from an RCL can be simultaneously shared by web and native client apps built using Blazor. The guidance in Consume ASP.NET Core Razor components from a Razor class library (RCL) explains how to share Razor components using a Razor class library (RCL). The same guidance applies to reusing Razor components from an RCL in a Blazor Hybrid app.

Component namespaces are derived from the RCL's package ID or assembly name and the component's folder path within the RCL. For more information, see ASP.NET Core Razor components. @using directives can be placed in _Imports.razor files for components and code, as the following example demonstrates for an RCL named SharedLibrary with a Shared folder of shared Razor components and a Data folder of shared data classes:

@using SharedLibrary
@using SharedLibrary.Shared
@using SharedLibrary.Data

Place shared static assets in the RCL's wwwroot folder and update static asset paths in the app to use the following path format:

_content/{PACKAGE ID/ASSEMBLY NAME}/{PATH}/{FILE NAME}

Placeholders:

  • {PACKAGE ID/ASSEMBLY NAME}: The package ID or assembly name of the RCL.
  • {PATH}: Optional path within the RCL's wwwroot folder.
  • {FILE NAME}: The file name of the static asset.

The preceding path format is also used in the app for static assets supplied by a NuGet package added to the RCL.

For an RCL named SharedLibrary and using the minified Bootstrap stylesheet as an example:

_content/SharedLibrary/css/bootstrap/bootstrap.min.css

For additional information on how to share static assets across projects, see the following articles:

The root index.html file is usually specific to the app and should remain in the Blazor Hybrid app or the Blazor WebAssembly app. The index.html file typically isn't shared.

The root Razor Component (App.razor or Main.razor) can be shared, but often might need to be specific to the hosting app. For example, App.razor is different in the server-side Blazor and Blazor WebAssembly project templates when authentication is enabled. You can add the AdditionalAssemblies parameter to specify the location of any shared routable components, and you can specify a shared default layout component for the router by type name.

Provide code and services independent of hosting model

When code must differ across hosting models or target platforms, abstract the code as interfaces and inject the service implementations in each project.

The following weather data example abstracts different weather forecast service implementations:

  • Using an HTTP request for Blazor Hybrid and Blazor WebAssembly.
  • Requesting data directly for a server-side Blazor app.

The example uses the following specifications and conventions:

  • The RCL is named SharedLibrary and contains the following folders and namespaces:
    • Data: Contains the WeatherForecast class, which serves as a model for weather data.
    • Interfaces: Contains the service interface for the service implementations, named IWeatherForecastService.
  • The FetchData component is maintained in the Pages folder of the RCL, which is routable by any of the apps consuming the RCL.
  • Each Blazor app maintains a service implementation that implements the IWeatherForecastService interface.

Data/WeatherForecast.cs in the RCL:

namespace SharedLibrary.Data;

public class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}

Interfaces/IWeatherForecastService.cs in the RCL:

using SharedLibrary.Data;

namespace SharedLibrary.Interfaces;

public interface IWeatherForecastService
{
    Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate);
}

The _Imports.razor file in the RCL includes the following added namespaces:

@using SharedLibrary.Data
@using SharedLibrary.Interfaces

Services/WeatherForecastService.cs in the Blazor Hybrid and Blazor WebAssembly apps:

using System.Net.Http.Json;
using SharedLibrary.Data;
using SharedLibrary.Interfaces;

namespace {APP NAMESPACE}.Services;

public class WeatherForecastService : IWeatherForecastService
{
    private readonly HttpClient http;

    public WeatherForecastService(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate) =>
        await http.GetFromJsonAsync<WeatherForecast[]?>("WeatherForecast");
}

In the preceding example, the {APP NAMESPACE} placeholder is the app's namespace.

Services/WeatherForecastService.cs in the server-side Blazor app:

using SharedLibrary.Data;
using SharedLibrary.Interfaces;

namespace {APP NAMESPACE}.Services;

public class WeatherForecastService : IWeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot"
    };

    public async Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate) =>
        await Task.FromResult(Enumerable.Range(1, 5)
            .Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            }).ToArray());
}

In the preceding example, the {APP NAMESPACE} placeholder is the app's namespace.

The Blazor Hybrid, Blazor WebAssembly, and server-side Blazor apps register their weather forecast service implementations (Services.WeatherForecastService) for IWeatherForecastService.

The Blazor WebAssembly project also registers an HttpClient. The HttpClient registered in an app created from the Blazor WebAssembly project template is sufficient for this purpose. For more information, see Call a web API from an ASP.NET Core Blazor app.

Pages/FetchData.razor in the RCL:

@page "/fetchdata"
@inject IWeatherForecastService ForecastService

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

Additional resources