One solution (setup) to rule them all – Blazor Webassembly, Blazor Server, Blazor Electron


Hello my dear friends and Blazorians πŸ™‚ This post will be a follow up on the previous post – Make it all dynamic in BLAZOR – Routing, Pages and Components

But before we start, I want to give a shout out to some of the great contributors in our BLAZOR community:

Ed Charbeneau, aka mr Powerglove
You will find his great stuff at github: https://github.com/EdCharbeneau
You will learn tons of blazor stuff, functional programming and CSS magic – You must check out his great streams at twitch: https://www.twitch.tv/edcharbeneau

csharpfritz (Jeff Fritz) – the guy who got horses to love javascript
You will find his great stuff at github: https://github.com/csharpfritz
A true artist when it comes to educate, inspire and share his knowledge. You need to check out his great twitch stream: https://www.twitch.tv/csharpfritz

Chris Sainty – the blog post generator
Check out all his great posts at https://chrissainty.com

Michael Washington – who has mastered the art of “blog post telling” with screen capturing
Check out all his great posts at http://blazorhelpwebsite.com

β„³isterβ„³aghoul / SQL-MisterMagoo – He has tried it all when it comes to BLAZOR.
Check out his great blazor stuff at github: https://github.com/SQL-MisterMagoo/

And there are so many more…

Ok, back to business good people πŸ™‚

My previous post described how to make Blazor apps with dynamic routes and pages. The secret is to decouple App.razor, layouts and components.
The cool thing with this approach is that it will be quite easy to run all Blazor variants(Webassembly, Server-side and Electron) in one solution setup.

So how is it done? Well… let me explain.
Here is the whole solution:

Right now we have three Blazor types/variants:

  • SitecoreBlazorHosted.Client – Blazor Webassembly
  • SitecoreBlazorHosted.Server – Blazor Server
  • SitecoreBlazorHosted.Electron – Blazor Electron
  • They have one thing in common. They are all referencing the Project.BlazorSite project, which contains App.razor and the layout/s. Project.BlazorSite is referencing the component libraries(Foundation and Feature layers).

    Let me walk you through each one of the Blazor types/variants.

    First out is Blazor Webassembly:

    SitecoreBlazorHosted.Client – this is a Blazor-Webassembly project. This means it will be running locally on the client but the data needs to be fetched/requested
    But most importantly, it will reference the Project.BlazorSite(where the layouts and the App.razor are located).

    Lets have a look at the Startup.cs. Notice how it uses the Project.BlazorSite.App.razor, that’s the beauty of a decoupled App.razor, layouts and components from the host project πŸ˜‰

     
    using Feature.Navigation.Extensions;
    using Foundation.BlazorExtensions.Extensions;
    using Foundation.BlazorExtensions.Services;
    using Microsoft.AspNetCore.Components.Builder;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace SitecoreBlazorHosted.Client
    {
    
        public class Startup
        {
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddScoped<IRestService, RestService>();
                services.AddForFoundationBlazorExtensions();
                services.AddForFeatureNavigation();
            }
    
            public void Configure(IComponentsApplicationBuilder app)
            {
                app.AddComponent<Project.BlazorSite.App>("app");
            }
        }
    }
    

    Here is also the Restservice instatiated, which will be used for fetching/request data. The Restservice is located in component library – Foundation.BlazorExtensions

     
    using System.Net.Http;
    using System.Text.Json;
    using System.Threading.Tasks;
    
    namespace Foundation.BlazorExtensions.Services
    {
        public class RestService : IRestService
        {
            private readonly HttpClient _httpClient;
            private readonly JsonSerializerOptions _jsonSerializerOptions;
    
            public RestService(HttpClient httpClient)
            {
                _httpClient = httpClient;
                _jsonSerializerOptions = new JsonSerializerOptions()
                {
                    IgnoreNullValues = true,
                    AllowTrailingCommas = true,
                    PropertyNameCaseInsensitive = true
                };
            }
    
            public async Task<T> ExecuteRestMethod<T>(string url) where T : class
            {
                return await ExecuteRestMethodWithJsonSerializerOptions<T>(url, _jsonSerializerOptions);
            }
    
            public async Task<string> ExecuteRestMethod(string url)
            {
                return await _httpClient.GetStringAsync(url);
            }
    
            public async Task<T> ExecuteRestMethodWithJsonSerializerOptions<T>(string url, JsonSerializerOptions? options)
            {
                string rawResultData = await ExecuteRestMethod(url);
    
                return JsonSerializer.Deserialize<T>(rawResultData, options ?? _jsonSerializerOptions);
            }
    
        }
    }
    

    In order to run, set SitecoreBlazorHosted.Client as StartUp project and select BlazorClient in Solution Configurations.
    *And don’t select IIS Express, why not try Kestrel instead(just select SitecoreBlazorHosted.Client). Now you will have a nice console window showing all the good stuff while the site is running.

    Easy peasy πŸ˜„

    Next is Blazor Server:

    SitecoreBlazorHosted.Server is a Blazor-Server project which means it runs on the client… Just kidding, on the server of course πŸ˜‰
    It will use SignalR for transporting DOM changes between server and client.
    If we look at the SitecoreBlazorHosted.Server.proj file you will see it’s a pure ASP.NET Core 3.0 app(TargetFramework is set to netcoreapp3.0). This means we can debug the solution in VisualStudio (or vscode).

     
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
        <LangVersion>8</LangVersion>
        <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
        <Configurations>Debug;Release</Configurations>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="BuildWebCompiler" Version="1.12.405" />
        <PackageReference Include="BuildBundlerMinifier" Version="3.0.415" />
        <PackageReference Include="Microsoft.AspNetCore.Components" Version="$(AspNetCoreVersion)" />
      </ItemGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\Project\BlazorSite\Project.BlazorSite.csproj" />
      </ItemGroup>
    
    </Project>
    

    And most importantly, it will reference the Project.BlazorSite(where the layouts and the App.razor are located).
    Lets have a look at _Host.cshtml, this is the page that will host the Blazor pages.

     
    @page "/"
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>BlazorSite server-side</title>
        <base href="~/" />
        <environment include="Development">
            <link href="css/site.min.css" rel="stylesheet" />
        </environment>
    </head>
    <body>
        <app>@(await Html.RenderComponentAsync<Project.BlazorSite.App>(RenderMode.Server))</app>
    
        <script src="_framework/blazor.server.js"></script>
        <script type="text/javascript" src="scripts/interop.min.js"></script>
    
    </body>
    </html>
    

    Notice the Html.RenderComponentAsync… Again see the power of having the App.razor, layouts and components decoupled πŸ˜‰

    Now to a very cool and interesting part, this app is running on the server. That means we can take advantage of fetching data directly on the server.
    Let’s have a look at the Startup.cs and try to locate IRestService:

     
    using Feature.Navigation.Extensions;
    using Foundation.BlazorExtensions.Extensions;
    using Foundation.BlazorExtensions.Services;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using SitecoreBlazorHosted.Shared;
    using System.Net.Http;
    using SitecoreBlazorHosted.Server.Providers;
    using SitecoreBlazorHosted.Server.Services;
    
    namespace SitecoreBlazorHosted.Server
    {
        public class Startup
        {
            // This method gets called by the runtime. Use this method to add services to the container.
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
            public void ConfigureServices(IServiceCollection services)
            {
                // HttpContextAccessor
                services.AddHttpContextAccessor();
                services.AddScoped<HttpContextAccessor>();
              
                services.AddRazorPages();
                services.AddServerSideBlazor();
    
                services.AddSingleton<HttpClient>((s) => new HttpClient());
                services.AddSingleton<IPathProvider, PathProvider>();
    
    
                services.AddScoped<IRestService, FilesIOService>();
                services.AddForFoundationBlazorExtensions();
                services.AddForFeatureNavigation();
    
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
         
    
                app.UseHttpsRedirection();
    
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapBlazorHub();
                    endpoints.MapFallbackToPage("/_Host");
                });
            }
        }
    }
    
    

    We are not using RestService here, instead we will use FilesIOService.

    FilesIOService will not make any rest call’s. It will get the data directly from the file system(on the server).

     
    using Foundation.BlazorExtensions.Services;
    using SitecoreBlazorHosted.Server.Providers;
    using System;
    using System.IO;
    using System.Text.Json;
    using System.Threading.Tasks;
    
    namespace SitecoreBlazorHosted.Server.Services
    {
        public class FilesIOService : IRestService
        {
            private readonly IPathProvider _pathProvider;
    
            private readonly JsonSerializerOptions _jsonSerializerOptions;
    
            public FilesIOService(IPathProvider pathProvider)
            {
                _pathProvider = pathProvider;
    
                _jsonSerializerOptions = new JsonSerializerOptions()
                {
                    IgnoreNullValues = true,
                    AllowTrailingCommas = true,
                    PropertyNameCaseInsensitive = true
                };
            }
    
            public async Task<string> ExecuteRestMethod(string url)
            {
                Uri uri = new Uri(url);
    
                string physicalFilePath = _pathProvider.MapPath(uri.AbsolutePath.TrimStart(new char[] { '/' }));
    
                if (!System.IO.File.Exists(physicalFilePath))
                    return string.Empty;    
                    
                using StreamReader sr = new StreamReader(physicalFilePath);
                return await sr.ReadToEndAsync();
    
            }
    
            public Task<T> ExecuteRestMethod<T>(string url) where T : class
            {
                return ExecuteRestMethodWithJsonSerializerOptions<T>(url);
            }
    
            public async Task<T> ExecuteRestMethodWithJsonSerializerOptions<T>(string url, JsonSerializerOptions options = null)
            {
                string rawResultData = await ExecuteRestMethod(url);
    
                return JsonSerializer.Deserialize<T>(rawResultData, options ?? _jsonSerializerOptions);
            }
        }
    }
    
    

    This is indeed very powerful!

    This could be a direct call/connection to a database or to Sitecore.
    Let’s hope Sitecore will move to Asp.Net Core in a very near feature πŸ˜‰

    To run server-side:
    Set SitecoreBlazorHosted.Server as StartUp project.
    Select BlazorServer in Solution Configurations.
    And run…

    And don’t forget, you can also debug it πŸ™‚

    Next up is Blazor-Electron.

    Project SitecoreBlazorHosted.Electron will allow us to run the Blazor solution as a desktop app.

    To make it work we need to host the Blazor app inside an Electron shell. There is this wonderful little gem at github, called AspLabs. This is where the Asp.Net Core team try out cool new things. Here we will find Components.Electron πŸ™‚

    So we need to create a local nuget package(following instructions from Components.Electron)
    by making a copy of the project, run it and produce the nuget package.
    The local nuget package, Microsoft.AspNetCore.Components.Electron.0.1.0-dev.nupkg, is located in the root of the solution.

    Lets take a quick look at the SitecoreBlazorHosted.Electron.proj file. We are referencing the local nuget package, notice also that it’s a pure ASP.NET Core 3.0 app(TargetFramework is set to netcoreapp3.0).

     
    <Project Sdk="Microsoft.NET.Sdk.Razor">
    
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
        <OutputType>WinExe</OutputType>
        <LangVersion>8</LangVersion>
        <SignAssembly>false</SignAssembly>
        <RazorLangVersion>3.0</RazorLangVersion>
        <ComponentsElectronVersion>0.1.0-dev</ComponentsElectronVersion>
        <DefaultItemExcludes>${DefaultItemExcludes};node_modules\**;package-lock.json</DefaultItemExcludes>
        <RestoreAdditionalProjectSources>
          ..\packages;
        </RestoreAdditionalProjectSources>
        <Configurations>Debug;Release;BlazorElektron</Configurations>
    
      </PropertyGroup>
    
     
      <ItemGroup>
        <PackageReference Include="BuildWebCompiler" Version="1.12.405" />
        <PackageReference Include="BuildBundlerMinifier" Version="3.0.415" />
        <PackageReference Include="Microsoft.AspNetCore.Blazor" Version="$(BlazorVersion)" />
        <PackageReference Include="Microsoft.AspNetCore.Components.Electron" Version="$(ComponentsElectronVersion)" />
      </ItemGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\Project\BlazorSite\Project.BlazorSite.csproj" />
      </ItemGroup>
    
      <Target Name="EnsureNpmRestored" BeforeTargets="CoreBuild" Condition="!Exists('node_modules')">
        <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
        <Exec Command="npm install" />
      </Target>
    
    </Project>
    

    And again… Most importantly, it will reference the Project.BlazorSite(where the layouts and the App.razor are located).

    Lets look at Startup.cs.

     
    using Feature.Navigation.Extensions;
    using Foundation.BlazorExtensions.Extensions;
    using Foundation.BlazorExtensions.Services;
    using Microsoft.AspNetCore.Components.Builder;
    using Microsoft.Extensions.DependencyInjection;
    using SitecoreBlazorHosted.Electron.Services;
    
    namespace SitecoreBlazorHosted.Electron
    {
        public class Startup
        {
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddScoped<IRestService, FilesService>();
                services.AddForFoundationBlazorExtensions();
                services.AddForFeatureNavigation();
                
    
            }
    
            public void Configure(IComponentsApplicationBuilder app)
            {
                app.AddComponent<Project.BlazorSite.App>("app");
            }
        }
    }
    
    

    And… We are calling the good old Project.BlazorSite.App.razor. The beauty of having App.razor, layouts and components decoupled… πŸ˜‰

    Because this is a desktop app we can access the local files similar like we did in the Blazor-Server project.
    This is FileService, which is located in SitecoreBlazorHosted.Electron:

     
    using System.IO;
    using System.Text.Json;
    using System.Threading.Tasks;
    using Foundation.BlazorExtensions.Extensions;
    using Foundation.BlazorExtensions.Services;
    
    namespace SitecoreBlazorHosted.Electron.Services
    {
        public class FilesService : IRestService
        {
            private readonly JsonSerializerOptions _jsonSerializerOptions;
            
            public FilesService()
            {
                _jsonSerializerOptions = new JsonSerializerOptions()
                {
                    IgnoreNullValues = true,
                    AllowTrailingCommas = true,
                    PropertyNameCaseInsensitive = true
                };
            }
    
            public async Task<string> ExecuteRestMethod(string url)
            {
    
    
                url = url.RemoveFilePrefix();
    
                if (!System.IO.File.Exists(url))
                    return string.Empty;
    
                using StreamReader sr = new StreamReader(url);
                return await sr.ReadToEndAsync();
            }
    
            public Task<T> ExecuteRestMethod<T>(string url) where T : class
            {
                return ExecuteRestMethodWithJsonSerializerOptions<T>(url);
            }
    
            public async Task<T> ExecuteRestMethodWithJsonSerializerOptions<T>(string url, JsonSerializerOptions options = null)
            {
                string rawResultData = await ExecuteRestMethod(url);
    
                return JsonSerializer.Deserialize<T>(rawResultData, options ?? _jsonSerializerOptions);
            }
    
        }
    }
    

    To run Electron App:
    Set SitecoreBlazorHosted.Electron as StartUp project.
    Select BlazorElectron in Solution Configurations.
    And run…

    And like the Blazor-Server project, we can also debug the Blazor-Electron project πŸ˜‰

    Ok guys, that was the three Blazor variants. But I have a feeling we will have a bunch of Blazor variants in a VERY near future. How about Steve Sandersson’s lates blog post: Exploring lighter alternatives to Electron for hosting a Blazor desktop app. Where Steve is looking into a smaller rendering stack, without any bundled Chromium or Node.js. If you guys are wondering who Steve is, he is the creator of Blazor… So expect nothing but great things from this miracle worker πŸ˜‰

    And don’t forget…

    Goodbye Javascript libraries/frameworks Hello Blazor

    The ongoing work happens in the Github project – SitecoreBlazor

    That’s all for now folks πŸ™‚


    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Google photo

    You are commenting using your Google account. Log Out /  Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s

    This site uses Akismet to reduce spam. Learn how your comment data is processed.