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 ๐Ÿ™‚


    7 thoughts on “One solution (setup) to rule them all – Blazor Webassembly, Blazor Server, Blazor Electron

    1. Thanks for this article it is really interesting ! I actually face the same kind of problem, i need to run my app in different envronment depending of the context. Your solution is perfect, I just have some questions :
      – there is a solution to “share” all the resources in the wwwroot folder ? (css, js, img, etc) actually it seem we have to copy everything in each projects
      – as i understand, the electron project run in blazor server-side. there is a way to run it in client-side (web assembly) ?

      Thanks again fo your work !

      Julien

      Liked by 1 person

      1. Hey Julien
        I’m glad you liked my post and solution.
        Hmm… Yes you could do it in the MSBUILD. If you look at Project – Feature.PageContent.csproj:
        https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Feature/PageContent/Feature.PageContent.csproj

        There is this:

         <Target Name="Client" BeforeTargets="PreBuildEvent">
            <Copy SourceFiles="@(SourceImages)" DestinationFiles="@(SourceImages->'$(BlazorClientDirectory)\wwwroot\images\%(RecursiveDir)%(Filename)%(Extension)')" ContinueOnError="false" />
          </Target>
        

        It copies image files from the project and put them at $(BlazorClientDirectory)\wwwroot\images\%(RecursiveDir)

        You could do something similar with the wwwroot stuff. Let’s say in the Project.BlazorSite.csproj:
        https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Project/BlazorSite/Project.BlazorSite.csproj

        Here you could have all the static files, wwwroot files and all that. And when you build(client, server or electron) you could copy it and put it in the right “project”. If you know what I mean ๐Ÿ™‚

        About Electron, why do you want it to run on web assembly ? I mean that is the cool thing with Electron you can run it as a windows project(Server-side)

        Like

        1. Thanks for the wwwroot solution !
          Concerning webassembly, the reason is i reuse ร  lot of code which i cant update for now (code run in another app) where there is static access which make the server side solution impossible if more than one person is connected. And i need wrap my app in รฉlectron stuff or other because i need develop ร  fonctionnality of watching local file downloaded and edited by the user To send it To the server when ils finish, which je impossible on web for now. So im stuck on this for now and i wait for a solution wich allow webassembly + intรฉgration in a desktop app, if you have other idea ?
          Thanks again for your help !

          Liked by 1 person

        2. Hello Julien
          You should see the Electron app as a webassembly app. Everything happens on the client, instead of using webassembley it runs on windows(like a windows app). This means NO server-side at all. So give it a try with Electron.
          Steve Sanderson(the father of BLAZOR) has an even better solution – Meet WebWindow, a cross-platform webview library for .NET Core, https://blog.stevensanderson.com/2019/11/18/2019-11-18-webwindow-a-cross-platform-webview-for-dotnet-core/
          Give it a try and let me know how it went ๐Ÿ™‚

          Like

    2. Hello gorhal, some return on your comments :
      – I seen the work of steven sanderson on his WebWindow. It’s amazing and the dll are directly compiled by the native app so in my case the static instance will work. I try to convert my app but im facing different problem, i have some dependency injection errors at startup, i have opened issues on the project repo. Moreover, there is no really concretes example on how the webapp can communicate with the native app and inverse (because the ultimate goal is this). So for the moment i let this project on the side
      – Electron : I’m a bit confuse on some things:
      *Is the first time is see this method for electron. i already test electron but with this package/solution : https://github.com/ElectronNET/Electron.NET. i succeed to use this with server side but was a little bit tricky with client-side, i suceed this but with really strange behavior. But if i understand your comment, shell a blazor server-side app in electron make the dll run locally, so “as” a client-side, true ?. I can’t run app twice in this method to check if the static instance are really separate between 2 app (i have dll arleady in use error at startup). I don’t understand why there is two separate solution.
      * Your solution : she works greate honestly, i can run the app two time and check the static class are really separate between the two app. So i will certainly begin a architecture like you. I just can’t really understand why in the electron app you read the file locally (i supposed here the dll run on the client but im not sure about that) rather read on the server. It’s because it’s just a file you include in the project ? What about a central database ? It seem we need to add a web server to receipt the request of the electron app, but i’m a bit confuse.

      and if you have the time, did you find example of interop between the webapp and a way to interact with the system ? (I suppose it will be nodejs code).

      thanks again for your time

      Julien

      Like

    3. other question no related : i begin to rework my code with your architecture, i begin to launch something but i have definitively a problem with the router, the page are not really recognized it seem. That’s why you have this system with the custom router ? it’s necessary ? (i don’t have multilanguage in my project so i don’t care about this part)
      thanks !

      Like

    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.