Shared Blazor WebAssembly and Hybrid code
The Blazor WebAssembly and Blazor Hybrid hosting models have a lot in common, and if you want to offer your application as both a SPA (or even a PWA) and a (hybrid) native application, you can share a lot of code between the two. Unfortunately, there is no project template with everything set up for you, but there is official documentation on how to do it yourself.
In this post, I'll take it a step further and show you how to put the two projects generated by the Blazor WebAssembly App and the .NET MAUI Blazor App templates into a single solution and move all their shared code into a third project created from the Razor Class Library project. If you want to follow the example, you should first create a new solution with three projects created using these templates.
Moving shared components to a shared library
The easiest part of this process is moving the components that are (almost) identical in both Blazor projects. These are all the components in the Shared
and Pages
folders, except for the FetchData
component, which is different between the two projects, so we'll cover that in a later step.
In the library project, create the folders of the same name and copy in all the files from the appropriate folders except FetchData.razor
. I suggest that you use the Blazor WebAssembly project as the source, since the pages in it contain the PageTitle
element, while the pages in the Blazor Hybrid project do not:
<PageTitle>Counter</PageTitle>
You want to keep this element to get the correct page titles in the web application. Having it in the hybrid application does not hurt.
There will be some imports missing in the copied files. I suggest you fix them by adding the imports to a newly created _Imports.razor
file in the root of the library.
You can now delete these components from both Blazor projects and add the library as a project reference. Even after this, the router still does not find the pages in the library. To fix this, you need to add the library assembly to the AdditionalAssemblies
attribute of the Router
element in the App.razor
(for Blazor WebAssembly) and Main.razor
(for Blazor Hybrid) files:
<Router
AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="new[] { typeof(Counter).Assembly }"
></Router>
Both Blazor apps should now build and run as they were before the change.
Moving static files to the shared library
There is another large group of identical files in both Blazor projects, namely all the static assets in the wwwroot
folders. Just like with components, you create a wwwroot
folder in the library and copy all the shared files from both projects to it. You should omit only the index.html
file (because it is different for each project) and the sample-data
folder (because it exists only in the WebAssembly project). Then delete the copied files from both Blazor projects again.
However, after you do this, the applications cannot load the assets. To fix this, you need to change the paths in the index.html
file. You need to add _content/LibraryName/
to the beginning of all existing paths, replacing LibraryName
with the actual name of your library, for example:
<link
href="_content/BlazorShared/css/bootstrap/bootstrap.min.css"
rel="stylesheet"
/>
<link href="_content/BlazorShared/css/app.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="_content/BlazorShared/favicon.png" />
Now the applications should work as before. For the web application, you may need to force a page refresh so that the browser reloads the updated index.html
file.
Moving the FetchData page to the shared library
Although you will most likely delete the FetchData page from your project at some point, it can serve as an example of how to have a shared page in the library when the underlying code for the two applications is different. For example, you could use this approach to get the user's current location: with the browser API in Blazor WebAssembly and with the native geolocation API in Blazor Hybrid.
This time, you should copy the page from the Blazor Hybrid application to the library, since the business logic is already included in a separate WeatherForecastService
. Also, you need to package the business logic from the Blazor WebAssembly project into a service class that implements the same interface that you should create in the library project:
public interface IWeatherForecastService
{
Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate);
}
Move the referenced WeatherForecast
class from the Blazor Hybrid application to the library as well. And change the import in the FetchData
component from the WeatherForecastService
class to the IWeatherForecastService
interface:
@inject IWeatherForecastService ForecastService
In the Blazor Hybrid application, you only need to change the service class to implement the new interface:
public class WeatherForecastService : IWeatherForecastService
{
// ...
}
Also change the dependency injection configuration in MauiProgram.cs
accordingly:
builder.Services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
More changes are required in the Blazor WebAssembly project. First, you need to create a service class that implements the IWeatherForecastService
, based on the code that was originally placed directly in the FetchData
component:
public class WeatherForecastService : IWeatherForecastService
{
private readonly HttpClient http;
public WeatherForecastService(HttpClient http)
{
this.http = http;
}
public async Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)
{
return await http.GetFromJsonAsync<WeatherForecast[]>(
"sample-data/weather.json");
}
}
Then you need to add this class to the dependency injection configuration in Program.cs
:
builder.Services.AddScoped<IWeatherForecastService, WeatherForecastService>();
And finally, you can delete the old FetchData
component, since it is now loaded from the library.
Moving the root component to the shared library
This last step may not be useful in all cases. By default, the root components in WebAssembly and Hybrid applications are identical, but depending on the characteristics of your application, they may be different. For example, you can not have a common root component unless you also move all your page components into the library, which means you can not have a different page for each project.
Since the root component has a different name in the two projects, you can choose its name in the library. I chose to copy the App component. Once you do that, you can also remove the AdditionalAssemblies
attribute from the Router
element, since all the page components are now necessarily in the shared library:
<Router AppAssembly="@typeof(App).Assembly"></Router>
In the Blazor Hybrid application, you need to change the reference to the root component in the MainPage.xaml
file:
<RootComponent Selector="#app" ComponentType="{x:Type local:App}" />
You must also change the local
namespace declaration to reference the shared library, e.g.:
<ContentPage xmlns:local="clr-namespace:BlazorShared;assembly=BlazorShared">
Now you can safely delete the old Main
root component in the Hybrid application.
The full source code of the final solution can be found in my GitHub repository. The individual commits represent the state of the project after each step, as described in this post. This should make it easier for you to review your changes if you decide to make them yourself.
In this post, I provided step-by-step instructions on how to set up a solution for sharing components and other code between a Blazor WebAssembly and a Blazor Hybrid project. If you are about to start a new project, you can simply copy the resulting code from my repository. However, if you already have a Blazor WebAssembly application that you also want to publish as a Blazor Hybrid application (or vice versa), you should be able to follow the steps described here to share the code between the two applications.