Blazor Lazy Loading and Dependency Injection

Lazy Loading

Many applications have functionality that is used infrequently. So why should your application load all the code for this feature if it is not being used?

In that case you should use lazy loading.

With lazy loading you can have your Blazor application load the code when it is actually needed. And enabling lazy loading is not hard at all!

Let us have a look at an example. The sources for this example is on my Github.

This single page application has two pages which are used infrequently.

Components

First step

The first thing you need to do is to move the components you want to lazy load to a razor component library. For the demo I have created a new Razor Component Library called LazyLoadingComponents, and I moved the LazyDog.razor and FetchData.razor component into it.

Once you have moved your components, test everything to see if it still works.

The Blazor runtime will still load this assembly on startup. Let us fix this.

Second step

Next step is to tell the compiler to use lazy loading for your library project. In your main project add the following ItemGroup, this will tell the runtime not to load the LazyLoadedLibrary.dll file when it starts:

<!-- List of lazy loaded assemblies -->
<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="LazyLoadedLibrary.dll" />
  <BlazorWebAssemblyLazyLoad Include="ServiceProxies.dll" />
</ItemGroup>

This tells the runtime not to load these assemblies when the application starts.

If this assembly has dependencies you can also mark them for lazy loading, here for example that would be the ServiceProxies class library.

Do not run the application. It will not function as expected.

There is one more step to do:

Third step

When the user navigates to your lazy loaded component you should load the assembly dynamically.

Who handles the navigation in your application? The Router of course!

Open App.razor (or wherever your router is).

Use dependency injection to get a reference to the LazyAssemblyLoader:

@inject Microsoft.AspNetCore.Components.WebAssembly.Services.LazyAssemblyLoader lazyAssemblyLoader;

Implement the OnNavigateAsync event on the Router component:

<Router AppAssembly="@typeof(Program).Assembly" 
        OnNavigateAsync="@OnNavigateAsync">

The router triggers this event whenever the user wants to navigate to another page, or when you call the NavigateTo method on the NavigationManager.

Implement this event to check the route, and if it uses a lazy loaded component, load the assembly. This example will load a razor component library, and a simple class library:

if (ctxt.Path == "lazy-loaded-component" || ctxt.Path == "fetchdata")
{
    var assemblies = await lazyAssemblyLoader.LoadAssembliesAsync(new[] { "LazyLoadedLibrary.dll", "ServiceProxies.dll" });
}

The router will inspect all Blazor components for your route at startup. But we just added a bunch of new components at runtime, so the router will not know about them. We need to tell the router about this, by giving it a collection of assemblies to inspect.

Add a List<Assembly> collection to your main component (normally App)

private List<Assembly> lazyLoadedAssemblies = new List<Assembly>()
{
};

Tell the router to check this collection for new assemblies using the AdditionalAssemblies parameter.

<Router AppAssembly="@typeof(Program).Assembly" 
        AdditionalAssemblies="@lazyLoadedAssemblies"
        OnNavigateAsync="@OnNavigateAsync">

And when you load a new assembly add it to the collection:

if (ctxt.Path == "lazy-loaded-component" || ctxt.Path == "fetchdata")
{
    var assemblies = await lazyAssemblyLoader.LoadAssembliesAsync(new[] 
      { 
        "LazyLoadedLibrary.dll", "ServiceProxies.dll"
      });
    lazyLoadedAssemblies.AddRange(assemblies);
}

Run the application. Everything should work as before! Or will it?

Lazy Loading and Dependency Injection

Lazy loading works very well for simple components.

But what if your component uses a dependency injected service and you want that service to be lazy loaded as well??

Let us look at an example:

The FetchData component of the example uses the IWeatherForecastService service.

public partial class FetchData
{
  [Inject]
  public IWeatherForecastService forecastService;

  private IEnumerable<WeatherForecast> forecasts;

  protected override async Task OnInitializedAsync()
  {
    forecasts = await forecastService.GetWeatherForecasts();
  }
}

As we have seen in the Lazy Loading section, we can move this component to our lazy loaded class library.

The WeatherForecastService is implemented in the ServiceProxies class library. Since we want to lazy load the WeatherForecastService dependency, we add this assembly to the list of lazy loaded assemblies:

<!-- List of lazy loaded assemblies -->
<ItemGroup>
	<BlazorWebAssemblyLazyLoad Include="LazyLoadedLibrary.dll" />
	<BlazorWebAssemblyLazyLoad Include="ServiceProxies.dll" />
</ItemGroup>

And we should not forget to add it in routing:

if (ctxt.Path == "lazy-loaded-component" || ctxt.Path == "fetchdata")
{
    var assemblies = await lazyAssemblyLoader.LoadAssembliesAsync(new[] 
      { 
        "LazyLoadedLibrary.dll",
        "ServiceProxies.dll
      });
    lazyLoadedAssemblies.AddRange(assemblies);
}

When you run this project, it will fail. This is because when we configure DependencyInjection in our Program.cs this line fails:

builder.Services.AddScoped< IWeatherForecastService, WeatherForecastService>();

The error tells us it cannot find the ServiceProxies assembly. This should not come as a surprise... We told the Blazor Runtime NOT to load it yet.

System.IO.FileNotFoundException: Could not load file or assembly 'ServiceProxies, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies

But now we are in a pickle... We cannot lazy load the assembly with the dependency if we want to configure dependency injection. DI become immutable after it has been configured, so we cannot postpone the configuration of the dependency...

The way to fix this is to use a class that is not lazy loaded, and that will create the dependency after we have loaded the assembly.

Start by creating a new library project (in my code called LazyLoaded). Then add a reference to the actual dependency (ServiceProxies). Next add a class LazyLoadedWeatherForecastService that will function as a Factory:

public class LazyLoadedWeatherForecastService
{
  private readonly IServiceProvider services;

  public LazyLoadedWeatherForecastService(IServiceProvider services)
  {
    this.services = services;
  }

  public IWeatherForecastService Create()
  {
    HttpClient http = this.services.GetRequiredService<HttpClient>();
    return new WeatherForecastService(http);
  }
}

This class will create the WeatherForecastService instance (and all its dependencies) when the Create method gets called. Please note that the Create method will cause the runtime to locate the WeatherForecastService type. The idea is that we will call this method after we have lazy-loaded the ServiceProxies assembly.

Now add a reference to this project (LazyLoaded) in the lazy-loaded libary.

<ProjectReference Include="..\LazyLoaded\LazyLoaded.csproj" />

Replace the dependency in your Program.cs

// builder.Services.AddScoped< IWeatherForecastService, WeatherForecastService>();
builder.Services.AddScoped<LazyLoadedWeatherForecastService>();

Replace the IWeatherForecastService dependency with the factory class:

@page "/fetchdata"
@inject LazyLoaded.LazyLoadedWeatherForecastService factory

And use it to create the actual dependency

protected override async Task OnInitializedAsync()
{
  forecasts = await factory.Create().GetWeatherForecasts();
}

Now when you navigate to fetchdata, the router will lazy-load the two assemblies (one with the razor components, one with the WeatherForecastService), create the component, run it. This will inject the LazyLoadedWeatherForecastService into the FetchData component, which uses it to create the actual dependency.

That all folks!

tweety