Skip to content

Effortless Blazor Lazy Loading and Dependency Injection

Notifications You must be signed in to change notification settings

RonSijm/RonSijm.Blazyload

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RonSijm.Blazyload

.NET NuGet Codecov

A C# Blazor library to effortlessly implement Lazy Loading and Dependency Injection

NuGet: https://www.nuget.org/packages/RonSijm.Blazyload/

What is this library

This library fixes all lazy-loading related things. Mainly dependency injection when dependencies are lazy-loaded.

Features

  • Have Standalone packages with Dependency injection working
  • Cascade loads dependencies so you don't have to worry about dll orchestration
  • Have An Optional Wrapper because Blazor won't let DI return null
  • Dynamically loading dlls without blazor telling you "You haven't registered that one as dll as lazyloaded"
  • Lazyload assemblies from alternative paths, other than /_framework/
  • Lazyload assemblies in an authenticated way
  • Lazyload assemblies from class, not just from the router
  • Fluxor integration for state management with lazy-loaded assemblies
  • Support for .NET 10+ fingerprinted assembly names

Demo / Tutorial video

Video of RonSijm.Blazyload

Here is a picture to better explain the purpose:

lazy-loading

Deployed Demo

https://ronsijm.github.io/RonSijm.Blazyload/


Getting Started

Basic Setup

In your Program.cs, configure Blazyload:

builder.UseBlazyload(BlazyConfig);

Where BlazyConfig is your configuration action. See the Navigation-Based Loading section for a complete example.

App.razor Configuration (V2 - Recommended)

This is the new and preferred way of configuring Blazyload. Configure your router to use the assembly loader directly - no code-behind file needed:

@using RonSijm.Blazyload
@inject IBlazyAssemblyLoader AssemblyLoader;

<Router AppAssembly="@typeof(App).Assembly"
        OnNavigateAsync="@AssemblyLoader.OnNavigateAsync"
        AdditionalAssemblies="@AssemblyLoader.AdditionalAssemblies">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Key Components Explained

  • @inject IBlazyAssemblyLoader AssemblyLoader; - Injects the Blazyload assembly loader service. This service manages lazy loading of assemblies and tracks which assemblies have been loaded.

  • OnNavigateAsync="@AssemblyLoader.OnNavigateAsync" - Connects the Router's navigation event to Blazyload. When the user navigates to a route, Blazyload checks if any assemblies are configured for that route (via LoadOnNavigation) and loads them automatically before the page renders.

  • AdditionalAssemblies="@AssemblyLoader.AdditionalAssemblies" - Provides the Router with a list of all loaded assemblies. This allows the Router to discover pages and components from lazy-loaded assemblies. As assemblies are loaded, they're automatically added to this collection.

That's it! No App.razor.cs code-behind file needed. The LoadOnNavigation calls in Program.cs handle all the routing logic:

// Automatically load assemblies when navigating to specific routes
options.LoadOnNavigation("fetchdata1", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
options.LoadOnNavigation("Lib1ReduceInto", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
options.LoadOnNavigation("Lib1ReduceIntoMethod", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
options.LoadOnNavigation("Lib1ReduceIntoReducer", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");

options.LoadOnNavigation("fetchdata4", "RonSijm.FluxorDemo.Blazyload.WeatherLib4.Page.wasm");

snippet source | anchor

V1 Style (Deprecated)

For historical reference, the old V1 style required a code-behind file with manual route handling:

/// <summary>
/// DEPRECATED: This is the old V1 style of manually handling navigation.
/// In V2, use LoadOnNavigation in Program.cs instead.
/// </summary>
public partial class App
{
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            // Old V1 style: manually check paths and load assemblies
            if (args.Path == "fetchdata1")
            {
                await AssemblyLoader.LoadAssemblyAsync("RonSijm.Demo.Blazyload.WeatherLib1.wasm");
            }
            else if (args.Path == "fetchdata2")
            {
                await AssemblyLoader.LoadAssemblyAsync("RonSijm.Demo.Blazyload.WeatherLib2.wasm");
            }
            else if (args.Path == "fetchdata3")
            {
                await AssemblyLoader.LoadAssemblyAsync("RonSijm.Demo.Blazyload.WeatherLib3.wasm");
            }
        }
        catch (Exception)
        {
            // Do Nothing
        }
    }
}

snippet source | anchor


Dependency Registration

Method 1: With IBootstrapper Interface (Recommended)

Create a BlazyBootstrap.cs class in your library's Properties folder:

// ReSharper disable once UnusedType.Global
public class BlazyBootstrap : IBootstrapper
{
    public Task<IEnumerable<ServiceDescriptor>> Bootstrap()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton<IWeatherResolver, WeatherResolver>();

        return Task.FromResult<IEnumerable<ServiceDescriptor>>(serviceCollection);
    }
}

snippet source | anchor

Method 2: Without Package Reference

If you don't want a reference to Blazyload in your library:

/// <summary>
/// In this example you can see how to bootstrap something without using a reference to the library.
/// Note that you must exactly implement "public Task&lt;IEnumerable&lt;ServiceDescriptor&gt;&gt; Bootstrap()"
/// </summary>
public class BlazyBootstrap
{
    // ReSharper disable once UnusedMember.Global
    public Task<IEnumerable<ServiceDescriptor>> Bootstrap()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton<IWeatherResolver, WeatherResolver>();

        return Task.FromResult<IEnumerable<ServiceDescriptor>>(serviceCollection);
    }
}

snippet source | anchor

Method 3: Custom Registration Class

Use a custom class name instead of the default BlazyBootstrap:

// ReSharper disable once UnusedType.Global
public class CustomRegistrationClass
{
    public Task<IEnumerable<ServiceDescriptor>> Bootstrap()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddSingleton<IWeatherResolver, WeatherResolver>();

        return Task.FromResult<IEnumerable<ServiceDescriptor>>(serviceCollection);
    }
}

snippet source | anchor

Configure it in Program.cs:

// Use a custom bootstrap class instead of the default BlazyBootstrap
options.UseSettingsForDll("RonSijm.Demo.Blazyload.WeatherLib3")
    .UseCustomClass("RonSijm.Demo.Blazyload.WeatherLib3.CustomRegistrationClass")
    .DisableCascadeLoading();

snippet source | anchor


Advanced Features

Cascade Loading

When you lazy load a library that has dependencies, those dependencies are automatically loaded. This is enabled by default.

To disable cascade loading for a specific assembly:

options.UseSettingsForDll("YourAssembly")
    .DisableCascadeLoading();

Custom Paths

Load assemblies from custom paths instead of the default /_framework/:

// Load assemblies from custom relative paths
options.UseSettingsForDll("RonSijm.Demo.Blazyload.WeatherLib1").UseCustomRelativePath("_framework/WeatherLib1/");
options.UseSettingsForDll("RonSijm.Demo.Blazyload.WeatherLib2").UseCustomRelativePath("_framework/WeatherLib2/").UseHttpHandler(dummyAuthHandler.HandleAuth);

snippet source | anchor

Criteria-Based Path Mapping

Use lambda expressions to match multiple assemblies:

// Use lambda expressions to match multiple assemblies by criteria
options.UseSettingsWhen(assembly => assembly.StartsWith("RonSijm.Demo.Blazyload.WeatherLib4", StringComparison.InvariantCultureIgnoreCase)).UseCustomRelativePath("_framework/WeatherLib4/");

snippet source | anchor

Authenticated Loading

Add authentication headers to assembly load requests:

// Load assemblies with authentication/custom HTTP handling
options.UseSettingsForDll("RonSijm.Demo.Blazyload.WeatherLib3").UseOptions(x =>
{
    // Note: absolute paths don't include the dll name, because these settings can be used for multiple dlls at once.
    // Note2: I'm not setting any custom url here, because the s3AuthHandler overrules it.
    //x.AbsolutePath = awsBucket;
    x.DisableCascadeLoading = true;
    x.ClassPath = "RonSijm.Demo.Blazyload.WeatherLib3.CustomRegistrationClass";
    x.HttpHandler = s3AuthHandler.HandleAuth;
});

snippet source | anchor

Navigation-Based Loading

Automatically load assemblies when navigating to specific routes:

// Automatically load assemblies when navigating to specific routes
options.LoadOnNavigation("fetchdata1", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
options.LoadOnNavigation("Lib1ReduceInto", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
options.LoadOnNavigation("Lib1ReduceIntoMethod", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
options.LoadOnNavigation("Lib1ReduceIntoReducer", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");

options.LoadOnNavigation("fetchdata4", "RonSijm.FluxorDemo.Blazyload.WeatherLib4.Page.wasm");

snippet source | anchor

Optional Dependencies

Use Optional<T> for dependencies that may not be loaded yet:

@inject Optional<IWeatherResolver> WeatherResolver

@code {
    protected override async Task OnInitializedAsync()
    {
        // Check if the optional dependency has a value before using it
        if (WeatherResolver.HasValue)
        {
            _forecasts = await WeatherResolver.Value.GetWeather();
        }
    }
}

Logging

Blazyload supports optional logging for debugging assembly loading issues:

builder.UseBlazyload(options =>
{
    // Enable general logging for assembly loading
    options.AssemblyLoaderOptions.EnableLogging = true;

    // Enable logging specifically for cascade loading errors
    options.AssemblyLoaderOptions.EnableLoggingForCascadeErrors = true;
});

Fluxor Integration

Blazyload has optional Fluxor integration available as a completely standalone package: RonSijm.Blazyload.Fluxor

Note: Blazyload works perfectly fine on its own without the Fluxor package. The Fluxor integration package is only needed if you want to lazy-load additional Fluxor states, reducers, and effects after the initial application load.

Setup with Fluxor

public static class DependencyInjectionService
{
    // Note: You don't *need* to declare your options like this, you can do it inside main.
    // I'm doing it like this so that my config accessible to bUnit
    public static Action<BlazyloadProviderOptions> CreateOptions(bool wireJavascriptServices = true)
    {
        var currAssembly = typeof(DependencyInjectionService).Assembly;

        var optionsFactory = new Action<BlazyloadProviderOptions>(options =>
        {
            // Automatically load assemblies when navigating to specific routes
            options.LoadOnNavigation("fetchdata1", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
            options.LoadOnNavigation("Lib1ReduceInto", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
            options.LoadOnNavigation("Lib1ReduceIntoMethod", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");
            options.LoadOnNavigation("Lib1ReduceIntoReducer", "RonSijm.FluxorDemo.Blazyload.WeatherLib1.wasm");

            options.LoadOnNavigation("fetchdata4", "RonSijm.FluxorDemo.Blazyload.WeatherLib4.Page.wasm");

            // Configure Fluxor for state management with lazy-loaded assemblies
            options.UseFluxor(fluxorOptions =>
            {
                fluxorOptions.ScanAssemblies(currAssembly);

#if DEBUG
                if (wireJavascriptServices)
                {
                    fluxorOptions.AddNativeExtension(x => x.UseReduxDevTools());
                }
#endif
            });
        });

        return optionsFactory;
    }
}

snippet source | anchor


RonSijm.Syringe

Historically, all dependency injection functionality was contained within the Blazyload library. For improved testability and separation of concerns, all non-Blazor-specific code has been moved to a standalone dependency injection framework: RonSijm.Syringe

Why Syringe?

  • Testability: By separating the core DI logic from Blazor-specific code, both libraries can be tested independently
  • Reusability: Syringe can be used in any .NET project, not just Blazor WebAssembly applications
  • Fully Compatible: Syringe is fully compatible with Microsoft's built-in dependency injection (Microsoft.Extensions.DependencyInjection)

Syringe Features Summary

Core Features

  • Implicit Wiring - Automatically register all classes in an assembly with WireImplicit<T>()
  • Dynamic Service Registration - Add services at runtime after the container is built with LoadServiceDescriptors()
  • Optional Dependencies - Optional<T> wrapper for dependencies that may not be available (especially useful in Blazor)
  • Lazy Resolution - Lazy<T> support for deferred service instantiation
  • Keyed Services - Full support for .NET 8+ keyed services
  • Scoped Services - Proper scope management with CreateScope()

Registration Control

  • Attribute-Based Registration - Control registration with [Registration.DontRegister] and [Lifetime.Singleton] attributes
  • Configuration-Based Registration - Configure service registration via appsettings.json
  • Assembly Bootstrapping - IBootstrapper interface for libraries to define their own service registrations

Extensibility

  • ILoadAfterExtension - Hook into assembly loading events
  • ISyringeAfterBuildExtension - Execute code after the service provider is built
  • ISyringeServiceProviderAfterServiceExtension - Decorate or modify services after resolution

Global Configuration

  • SyringeGlobalSettings - Configure default service lifetime and registration behavior

For full documentation, see the RonSijm.Syringe repository.


Fingerprinting Support

Starting with .NET 10, Blazor WebAssembly uses fingerprinted assembly names (e.g., MyAssembly.abc123.wasm) for cache busting. Blazyload automatically handles this by reading the fingerprint mapping from the Blazor runtime configuration.

How it works

Blazyload uses JavaScript interop to read the assembly mapping directly from the Blazor runtime's in-memory configuration. This is more efficient than fetching and parsing configuration files via HTTP.

Disabling Fingerprinting

If you need to disable fingerprinting for your project, you can do so in your .csproj file:

<PropertyGroup>
    <WasmFingerprintAssets>false</WasmFingerprintAssets>
</PropertyGroup>

Breaking Changes

.NET 10+

The blazor.boot.json file has been removed and its configuration is now inlined into dotnet.js. Blazyload handles this automatically by reading the configuration from the Blazor runtime's memory via JavaScript interop.

See: What's new in ASP.NET Core 10.0 - blazor.boot.json inlined

.NET 8+

Instead of referencing your libraries as .dll, they now have a .wasm extension. See: dotnet/runtime#92965 (comment)

Changelog

  • Blazyload 2.0: Dependency injection abstracted and moved to RonSijm.Syringe, support for .NET10
  • Blazyload 1.3: Loading PDB Symbols while debugger is attached
  • Blazyload 1.2: Added Support for .NET 8

Contributing

  • Bugfixes: Submit a PR with a bugfix + a unit-test
  • Features: Start a discussion first

Contact

Discord: https://discord.gg/cDC6VkUn2X

About

Effortless Blazor Lazy Loading and Dependency Injection

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published