🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

ASP.NET Core MVCにServer side Blazorのコンポーネントを共存させる話

本エントリはBlazor Advent Calender 2019の4日目の記事です

はじめに

.NET Core 3.0がリリースされてから3か月近く経過しました。
※追記(12/04):本日.NET Core 3.1 LTSがリリースされました!

皆さんはお仕事でBlazor書いてますでしょうか?勿論私は書けていません。
産まれて間もないですし、中々難しいところではあります。
※海外ではBlazorチョットデキル人への求人がちょこちょこ出てきているようですが...。

どうにかBlazorを合法的に使用できないものか?

弊社の場合フルスクラッチでBlazorのSPAを作成するのは政治的な理由で難しく、既存のASP.NET Core MVCにComponent単位で組込む程度が現実的でしょう。
※BlazorのComponent(Razor Componenet)は普通のASP.NET Core MVCでも表示可能なので、SignalRとの通信さえできればMVC上でServer side Blazorできるはず。

というわけで「既存のASP.NET Core MVCをComponent単位でBlazorに置き換えてく作戦」で行きたいと思います。

留意点

とはいえ、手放しにServer side Blazorを組込むことは出来ません。
何個か留意点があります。

  1. SignalRと常時通信が発生するためクライアント側に安定した接続環境が必要となる。

  2. SignalRとの通信タイムアウトやBlazorのエラーハンドリングが必要 ※最新のBlaozrなら<div id="blazor-error-ui"></div>とかが用意されてるので苦ではないかも。

  3. サーバーのスペックとかスケーリングの見積

ちなみに、3番についてはMSの人が多少検証したっぽいです。

devblogs.microsoft.com

上記Blogから引用します。

In our tests, a single Standard_D1_v2 instance on Azure (1 vCPU, 3.5 GB memory) could handle over 5,000 concurrent users without any degradation in latency. A Standard_D3_V2 instance (4 vCPU, 14GB memory) handled well over 20,000 concurrent clients. The main bottleneck for handling further load was available memory.

メモリ量が大事っぽいですね。

では、まずはRazor Componentの表示からやってみましょう。

ASP.NET Core MVC Projectの作成

ベースとなるASP.NET Core MVCプロジェクトを作成します。
※筆者の開発環境は.NET Core 3.1 Preview3です。 ※追記(12/04):本日リリースされた.NET Core 3.1 LTSでOKです!

dotnet new mvc -o BlazorOnMVC
cd BlazorOnMVC

Views/Home/BlazorComponent.razorの作成

Blazorのコンポーネントを作成します。

<h1>Hello @Name!</h1>

<p>This is Blazor Components.</p>

@code {
    [Parameter]
    public string Name {get; set;}
}

Views/Home/Index.cshtmlの編集

作成したRazor Componentを<component />で表示させます。
この辺りは.NET Core 3.1 Preview2で変更が入った書き方ですね。

param-Nameで文字列を引数として渡していますが
param-Name="Blazor"だとerror CS0103: 現在のコンテキストに Blazor という名前は存在しません。と怒られます。
しゃーないのでStringを@("文字列")で括って与えています。

+ @using BlazorOnMVC.Views.Home

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

+ <component type="typeof(BlazorComponent)" render-mode="ServerPrerendered" param-Name="@("Blazor")"/>

ちなみに過去のバージョンでのRazor Componentの表示方法は以下の通りです。

// .NET Core 3.0 Preview8 までの書き方
@(await Html.RenderComponentAsync<BlazorComponent>())

// .NET Core 3.0 Preview9 から.NET Core 3.1 Preview1 までの書き方
@(await Html.RenderComponentAsync<BlazorComponent>(RenderMode.ServerPrerendered))

// .NET Core 3.1 Preview2 からの書き方
<component type="typeof(BlazorComponent)" render-mode="ServerPrerendered" /> 

とりあえず以上です。

Razor Componentが描画されるかどうか、実行してみます。

実行結果

ASP.NET Core MVCでRazor Componentの表示に成功しました。

f:id:piyo_esq:20191202215954p:plain
Razor Component on ASP.NET Core MVC

続いてSignalRと通信するための部分を追加します。

Startup.csの編集

Startup.csを編集します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
+   services.AddServerSideBlazor();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

    // 途中省略
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
+       endpoints.MapBlazorHub();
    });
}

Views/Home/Index.cshtmlの編集

blazor.server.jsを埋め込みます。

_Layout.cshtmlに埋め込んでも良いですが、Razor Componentを使用していないページでもSignalRとの通信が発生してしまいます。 よってRazor Componentを使用するページであるIndex.cshtmlblazor.server.jsを埋め込みます。

@using BlazorOnMVC.Views.Home
+ <script src="_framework/blazor.server.js" autostart="false"></script>

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<component type="typeof(BlazorComponent)" render-mode="ServerPrerendered" param-Name="@("Blazor")" />

+ <script>
+     window.onload = function() {
+         Blazor.start({
+             configureSignalR: function (builder) {
+                 builder.configureLogging(2);  // 2はLogLevel.Information
+             },
+             logLevel: 2,
+             reconnectionOptions: {
+                 maxRetries: 3,
+                 retryIntervalMilliseconds: 2000,
+             }
+         });
+     }
+ </script>

_Layout.cshtmlならautostart="true"で正常に動作しますが、Index.cshtmlだとうまく動作しませんでした。
仕方ないのでautostart="false"を指定しwindow.onloadのタイミングでSignalRとの通信を開始しています。ライフサイクルの関係でしょうか...。

なおautostartは省略可能です。省略した場合autostart="true"で動作します。
ソース的にはこことかここら辺っぽいですね

LogLevelの指定はこちらをご参照ください。

docs.microsoft.com

BlazorComponent.razorの編集

Razor Componentに手を加えます。
ボタンを押下した回数を画面に表示する処理を追加しました。

注意点としてはMicrosoft.AspNetCore.Components.Webを参照に追加するのを絶対に忘れないでください。
上記参照がない状態だと、ボタンを押下しても@onclickにbindしたメソッドが実行されません。しかもエラーも何も表示されません...😖

+ @using Microsoft.AspNetCore.Components.Web 
<h1>Hello @Name!</h1>

<p>This is Blazor Components.</p>

+ <p>Counter: @Count</p>
+ <button @onclick="IncrementCount">Count++</button>

@code {
    [Parameter]
    public string Name {get; set;}

+   int Count = 0;
+   void IncrementCount()
+   {
+       Count++;
+   }
}

実行結果

dotnet runで起動します。
WebSocket connectedって書いてありますね。

f:id:piyo_esq:20191202215004p:plain
dotnet run

念のためNetworkタブを見たところ、ちゃんとSignalRと通信してるっぽいです。
数秒おきにSignalRとHeartBeatしてるログが表示されています。

f:id:piyo_esq:20191202215027p:plain
HeartBeat Log

Count++ボタンを押下すると画面上のCountの数値がインクリメントされていきます。
Server side Blazorが動きました!

おわり

というわけで、無事ASP.NET Core MVCにServer side Blazor(Razor Component)を乗せることに成功しました。「既存のASP.NET Core MVCをComponent単位でBlazorに置き換えてく作戦」は実現可能っぽいですね。

今回はRazor Componentを組込むだけですが、MapFallbackToPageを使えばMapControllerRouteによるRoutingを保ちつつ一部のページをRazor PageでSPA化もできるのかな?いずれチャレンジしてみたいと思います。。

以上です。