.NET で SPA が書ける Blazor の使い方をざっくり解説
Blazor というのは、SPA のクライアントアプリを .NET で書くと、WebAssembly 上で .NET のバイナリのまま動かすことができるというフレームワークだ。
一見かなりアクロバティックな仕組みだし、.NET 界隈だと Silverlight を思い出して「うっ頭が…」となる人もいるようだが、まあ今回は独自プラグインではなく、Web 標準 である WebAssembly にちゃんと乗ってるところが大きな違い。
完成度としては、「実験的なプロジェクトなので Production では使わないこと」とうたってはいるが、実際使ってみるとすでに基本的な機能はかなりできあがっていてそこそこちゃんと動くので、使い方をざっくりまとめておく。
※ちなみに Blazor という名前の由来は、公式 FAQ によると "Browser + Razor = Blazor" とのこと。なぜ二文字目がRじゃなくてLなのかは謎。
開発環境の構築
.NET Core 2.1 RC SDK のインストール
まずは、.NET Core 2.1 SDK の最新版をインストールする。2018年5月17日時点で最新バージョンは2.1.300-RC1。
Visual Studio 2017 Preview のインストール
.NET Core 2.1 を使うには、Preview 版の Visual Studio が必要となる。以下のリンクからインストールする。
なお Preview 版は、通常版の Visual Studio 2017 と同居させられる作りになっている。通常版を利用している人も何も考えずにインストールしてしまって大丈夫。 同居させると、インストーラの画面はこんな感じになる。
テンプレートからプロジェクトを新規作成
プロジェクトの新規作成で、「ASP.NET Core Web アプリケーション」から、「Blazor (ASP.NET Core hosted)」を選択する。
今回は「Blazor (ASP.NET Core hosted)」を選択して、サーバサイドの Web API を含んだソリューションを作成するが、すでに Web API が別にあり、純粋にクライアントサイドのみ開発したい場合には、クライアントアプリのみのプロジェクトが生成される「Blazor」を選択する。
※「Blazor」を選択した場合、ビルドして生成されるのは単なる静的ファイル群なので、静的な Web サイトをホストできるサービスであればどこにでも配置して公開できる。一方「Blazor (ASP.NET Core hosted)」の場合は、当然ながら ASP.NET Core が動く環境でである必要がある。
なおいずれの場合も、ランタイムは ASP.NET Core 2.1 または 2.0 を選択すること。
プロジェクトを作成すると、以下のようなプロジェクト構成のソリューションが作成される。
- [プロジェクト名].Client
- [プロジェクト名].Server
- [プロジェクト名].Shared
ほぼ自明ではあるが、それぞれ以下のような役割のプロジェクトとなる。
.Client
Blazor を使ったクライアントアプリの本体部分。
見た目上は、.cshtml
ファイルが Pages
フォルダ内に配置されていて、Razor Pages のプロジェクトっぽい(Blazor が Razor の拡張なので当たり前といえば当たり前)。
Blazor 用に以下の NuGet パッケージがインストールされている。 - Microsoft.AspNetCore.Blazor.Browser - Microsoft.AspNetCore.Blazor.Build
.Server
サーバサイドの Web API。 中身は普通の ASP.NET Core Web API なので、特に難しいところはない。
実行時には、.Client は、この .Server プロジェクトの ASP.NET Core 上にホスティングされる状態になる。
具体的には、.Server プロジェクトから .Client プロジェクトを参照し、さらに以下のように .Server プロジェクトの Startup
クラス Configure()
内で app.UseBlazor<T>()
を呼ぶことで、ホスティングする Blazor クライアントプロジェクトを指定している。
Startup.cs
public class Startup { public void ConfigureServices(IServiceCollection services) { ... } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseBlazor<Client.Program>(); } }
.Shared
クライアントとサーバ両方から参照されるクラスライブラリ。 主にはクライアントとサーバでやりとりする JSON をシリアライズするためのクラスを配置するのに使用する。
とりあえず実行してみる
スタートアッププロジェクトは .Server を選択して、デバッグ実行する。 初期状態では、クライアント単独で動くカウンタと、Web API から取得した天気情報を表示するサンプルページが用意されている。
なお、デバッガはまだ開発中なので、Blazor のコンポーネント内にブレイクポイントを置いても動作しない。
ここまででセットアップは終わりなので、あとはアプリを書いていくだけだ。 以降では、Blazor アプリの書き方の基本を少し説明する。
Blazor アプリの書き方
公式の FAQ に、"inspired by existing modern single-page app frameworks, such as React, Angular, and Vue" とあるだけあって、React などの JavaScript の UI フレームワークの考え方がいろいろごっちゃになって取り入れられている。
基本的な使い方としては、C#/HTML からなるコンポーネントという塊を作り、それを入れ子にして組み合わせることでアプリケーションを構成する、というおなじみの仕組みになっている。
コンポーネントを作成するには、以下の3つのやり方がある。
- インライン (.cshtml) 方式
- コードビハインド方式
- クラス方式
まずはとりあえず、一番わかりやすいインライン方式で説明する。
インライン方式でのコンポーネント作成
インライン方式では、一つの .cshtml ファイルが一つのコンポーネントとなる。 .cshtml ファイルなだけあって、従来の Razor 構文で View を記述すれば良いのだが、大きな違いとして、Blazor コンポーネントでは「コンポーネント変数」を持つことができる。
コンポーネント変数は、.cshtml の @functions{}
内で[Parameter]
属性を付けた変数として宣言する。
これにより、React における Props のように、コンポーネントを使う親コンポーネント側で属性値として値を渡すことができる。
また、コンポーネント名のタグに挟まれたテキストの値は、RenderFragment
型の ChildContent
という名前のコンポーネント変数(名前は変更不可)を定義しておくことで、受け取ることができる。
Child.cshtml
<!-- 子コンポーネント --> <h1>@Title</h1> <p>@ChildContent</p> @functions { [Parameter] private string Title { get; set; } [Parameter] private RenderFragment ChildContent { get; set; } }
Parent.cshtml
<!-- 親コンポーネント -->
<Child Title="ここでタイトルを渡す">ここでChildContentを渡す</Child>
親から子には、Action を渡すこともできる。
これを使うと、React 風に、Presentational/Container でコンポーネントを分け、子コンポーネント(Presentational)で発生したイベントを親コンポーネント(Container)に伝えて、ロジックは親側に集約するようなことができる。
子コンポーネントでは、渡された Action を任意の契機で Invoke()
すれば良い。
ただし、イベント契機で親が管理する値を変更した場合、手動で StateHasChanged()
を叩かないと、画面上に変更は反映されないことに注意する。
非公式ドキュメントには、StateHasChanged()
は親と子で独立に動くので、親と子両方に反映するには双方で呼ぶ必要があるという記載があるが、試した限りでは、親側でさえ呼べば、子側にも変更が反映された。
以下のサンプルでは、子側が持つ「Count!」ボタンを押下すると、親側の Action が実行されて currentCountOnParent
がカウントアップされ、その変更が子側にも伝播することで、両方の表示が更新される。
ParentComponent.cshtml
@page "/parentComponent" <h1>親子での連携テスト</h1> <p>Count on Parent: @currentCountOnParent</p> <ChildComponent currentCountOnChild=@currentCountOnParent OnChildButtonClicked=@ChildButtonClicked /> @functions { private int currentCountOnParent = 0; private void ChildButtonClicked() { Console.WriteLine("ChildButton is clicked"); currentCountOnParent++; // 手動で描画を更新する StateHasChanged(); } }
ChildComponent.cshtml
<p>Count on Child: @currentCountOnChild</p> <button onclick=@OnClick>Count!</button> @functions { [Parameter] private int currentCountOnChild { get; set; } [Parameter] private Action OnChildButtonClicked { get; set; } private void OnClick() { OnChildButtonClicked?.Invoke(); } }
インライン方式以外のコンポーネント作成方法
インライン方式以外での記述方法は以下のとおり。
コードビハインド方式
C# のロジック部分と HTML(Razor) を分けて記述する方式。
具体的には、C# 部分は BlazorComponent
を継承するクラスに記述し、View 部分のみ .cshtml に記述した上で、@inherits
文を使ってロジックのクラスを指定する。
ChildBase.cs
public class ChildBase : BlazorComponent { [Parameter] private string Title { get; set; } [Parameter] private RenderFragment ChildContent { get; set; } }
ChildByCodeBehind.cshtml
@inherits ChildBase <h1>@Title</h1> <p>@ChildContent</p>
クラス方式
純粋に C# のクラスとしてコンポーネントを作成することもできる。
実は、他の 2 方式で作ったコンポーネントも、プロジェクトをビルドすると [コンポーネント名].g.cs
というファイル名でこの形式のクラスに自動的に変換されている。
クラス内の BuildRenderTree()
で、HTML の要素を一つずつ組み立てていく必要があるため、必要がない限りあえて用いる必要はない。
ChildByClass.cs
public class ChildComponent : BlazorComponent { private string Title { get; set; } private RenderFragment ChildContent { get; set; } protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(1, "h1"); builder.AddContent(2, Title); builder.CloseElement(); builder.OpenElement(3, "p"); builder.AddContent(4, ChildContent); builder.CloseElement(); } }
ルーティング
作成したコンポーネントにアクセスするためには、ルーティングを定義してやる必要がある。
インライン方式・コードビハインド方式の場合は、.cshtml の先頭に @page "/path/to/component"
という形式で宣言してあげるだけで良い。
Parent.cshtml
@page "/parent" @page "/parent2" // 一つのコンポーネントに複数のパスを定義することもできる @page "/parentWithParam/{parentId}" // URLからパラメータを受け取ることもできる <!-- 親コンポーネント --> <Child Title="ここでタイトルを渡す">ここでChildContentを渡す</Child> @functions { [Parameter] private int parentId { get; set; } }
クラス方式の場合は、[RouteAttribute()]
を使用する。
[RouteAttribute("/parent")] [RouteAttribute("/parent2")] [RouteAttribute("/parent3/{parentId}")] // これは未確認… public class ParentComponent : BlazorComponent { ... }
ライフサイクルメソッド
これも React などと似た感じで、コンポーネントのライフサイクルメソッドが用意されている。 実際には違う点も多いが、イメージをつかむための参考までに類似の React メソッドも記載している。
メソッド名 | (参考:類似の React メソッド) | 用途 |
---|---|---|
OnInit() |
componentWillMount() |
コンポーネントの初期化直後に一度だけ呼び出される |
OnParametersSet() |
componentWillReceiveProps() |
コンポーネント変数が割り当て(変更も含む)されるごとに呼び出される |
OnAfterRender() |
componentDidUpdate() |
コンポーネントの初期描画が完了、または更新されるごとに呼び出される |
ShouldRender() |
ShouldRender() |
パフォーマンスチューニングなどで描画を更新したくない場合、ShouldRender() が false を返すようにする |
使う際には、対象のコンポーネント内で以下のようにメソッドを override すれば良い。
@functions { protected override void OnInit() { Console.WriteLine("OnInit() called"); } protected override void OnParametersSet() { Console.WriteLine("OnParametersSet() called"); } protected override void OnAfterRender() { Console.WriteLine("OnAfterRender() called"); } }
おまけ:Blazor で Flux するには
たぶん Blazor で Flux しようとする人はいるんだろうなと思ってググったところ、blazor-redux というプロジェクトがすでに存在していた。
Redux 風の API を C# で実装していて、Redux を知っていればすぐに使えそう。 ちゃんとブラウザ上で Redux DevTools を使って State を見られるようにしてあるようでえらい。
で、こういうのが必要かどうかっていうのは結局 JS のフロント開発と一緒で、開発規模がかなり大きくなれば、必要になってくるんだろうとは思う。
参考
公式ドキュメント
有志が作っているチュートリアルサイト
- Blazor コンポーネントについての記事
Microsoft Graph(Microsoftクラウドサービスの統合API)の使い方
Microsoft Graph を使うと、OneDrive (for Business) や SharePoint, Outlook, Teams 等を含む多くの Microsoft のクラウド製品を、標準化された REST API 経由で操作できる。 developer.microsoft.com
何ができるかはの詳細は上記リンク先に記載されているが、以下のようなリソースの主要な操作はだいたい行うことができる。
- Users/Groups
- Outlook
- OneDrive (for Business)
- Azure AD
- OneNote
- SharePoint
- Teams
- 等々...
今回は例として、O365を契約している前提で、OneDrive for Business からファイルを取得する場合の手順を説明する。 利用する際は、REST API を直接叩くこともできるが、SDK が提供されている言語の場合は、SDK を使うのが楽で良い。提供されている SDK は以下のとおり。今回は、.NET SDK を利用する。
目次
1. アプリの登録
事前に、Graph にアクセスするアプリケーションを認証基盤側に登録する必要がある。
1.1 アプリの作成
Azure AD の認証エンドポイントには、v1.0 と v2.0 の 2 つがあり、それぞれアプリ登録を行う場所が異なる(v1.0 と v2.0 の違いについては後述)。今回は v1.0 を利用するので、Azure Portal の Azure AD 管理画面からアプリ作成を行う。
詳細な手順については、以下を参考にすると良い。 docs.microsoft.com
「アプリケーションの種類」は「Web アプリ/API」を選択する。また、作成したアプリの画面に表示される アプリケーションID
が、後ほど使う Client ID
となるので、メモしておく。
1.2 Client Secret の生成
アプリ作成後には、メニューからキーを生成しておく。これが後ほど用いるClient Secret
となるので、同じくメモしておく。
1.3 アプリへの権限付与
さらに、作成したアプリに対して、Graph で利用したいリソースへのアクセス許可を付与する。 アプリの「設定」から「必要なアクセス許可」→「追加」と進み、API として「Microsoft Graph」を、アクセス許可としては「アプリケーションのアクセス許可」から「Read all groups」を選択して保存する。
そのうえで、「必要なアクセス許可」の画面から再度「Microsoft Graph」を選択し、「アクセス許可の付与」→「はい」を選択する。
なお、あとから権限を削除したい場合も、削除したいアクセス許可のチェックを外して保存したあとに、同様に「アクセス許可の付与」を再度行う必要があるので注意する。
行いたい処理に対して、どのようなアクセス許可が必要かは、以下のドキュメントで確認できる。
アクセス許可 - ドキュメント - Microsoft Graph
2. アプリ実装
2.1 認証の流れ
アプリの登録が終わったら、Visual Studio でコンソールアプリを作成し、以下の NuGet パッケージをインストールする。
Microsoft.IdentityModel.Clients.ActiveDirectory
Microsoft.Graph
事前準備で作成したアプリの Client ID, Client Secret を使って認証トークンを Azure AD から取得し、そのトークンを使って認証する Graph 用クライアントのインスタンスを作成する。
// TODO: "hogehoge"部分を、アクセス先となる Azure AD テナントの名前に置き換える var authority = "https://login.microsoftonline.com/hogehoge.onmicrosoft.com"; var authContext = new AuthenticationContext(authority, null); // TODO: Azure Portal で作成したアプリの Client ID, Client Secret に置き換える var clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; var clientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; var clientCredential = new ClientCredential(clientId, clientSecret); // 指定したリソースへのアクセス権限を持った認証トークンを取得する var graphResourceUri = "https://graph.microsoft.com"; var authResult = await authContext.AcquireTokenAsync(graphResourceUri, clientCredential); var accessToken = authResult.AccessToken; // リクエスト送信時に認証トークンを利用するように設定した、GraphServiceClient のインスタンスを作成する var graphServiceClient = new GraphServiceClient( new DelegateAuthenticationProvider((requestMessage) => { requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); return Task.FromResult(0); }) );
あとは、作成した GraphServiceClient
のインスタンスを使って、Microsoft Graph へのリクエストを送信する。
// Group のリストを取得 var groups = await graphClient.Groups.Request().GetAsync(); // 各 Group のデフォルトの Drive と、Drive のルートにあるファイル・フォルダのリストを取得する foreach(var group in groups) { Console.WriteLine($"Group Name: {group.DisplayName}, Group ID: {group.Id}"); var drive = await graphClient.Groups[group.Id].Drive.Request().GetAsync(); Console.WriteLine($"Drive Name: {drive.Name}, Drive ID: {drive.Id}"); var rootItems = await graphClient.Drives[drive.Id].Root.Children.Request().GetAsync(); Console.WriteLine($"Items in the root folder:"); foreach(var item in rootItems) { Console.WriteLine($" - {item.Name}"); } }
2.2 例外ハンドリング
GraphServiceClient
は ServiceException
型の例外を投げる。ServiceException
の持つ IsMatch()
を使うと、エラーの種別を判別することができる。
try { ... } catch(ServiceException ex) { if(ex.IsMatch(GraphErrorCode.ItemNotFound.ToString())) { Console.WriteLine("指定したファイルが見つかりませんでした。"); } }
この場合、ItemNotFound
のエラーなので、その旨のエラーメッセージを出力している。
その他 GraphErrorCode
が持つエラーの種別については、以下に一覧が記載されている。
https://developer.microsoft.com/en-us/graph/docs/concepts/errors
3. 補足
3.1 Web アプリとネイティブアプリでの認証の違い
少し話は脱線するが、Web アプリや Web API から Graph にアクセスする場合と、デスクトップ・スマホなどネイティブアプリから直接 Graph にアクセスする場合で、用いるべき認証の方式が異なる。
前者の場合は、Client ID
とClient Secret
の組み合わせで認証される"Service Principal"と呼ばれるアプリを示すアカウントの権限を用いて、Graph にアクセスすることができる。一方後者の場合は、Client Secret
を安全に秘匿することができないため、実際にアプリを使うユーザが自身のアカウントを使って Azure AD の画面からログインすることで、そのユーザ自身の権限を用いて Graph にアクセスする。
今回のサンプルでは、説明の都合上コンソールアプリで Service Principal を利用しているが、実利用においては正しく使いわけること!
3.2 Azure AD v1.0 と v2.0 の違い
Azure AD v1.0 と v2.0 の違いは以下に詳しく説明されている。 docs.microsoft.com
大きな違いとしては、以下のような点がある。
- v1.0 は Azure AD アカウントのみをサポートしていたのに対して、v2.0 ではさらに Microsoft Account もサポートしている
- 認証に使うライブラリも、v1.0 は従来の ADAL を利用するのに対して、v2.0 では MSAL と呼ばれる、新しい認証ライブラリを使う
UnityやAutofacでDIする際に、同じ型の複数のインスタンスを注入する方法
.NETで開発をしていると、クラスを別の依存クラスの実装と分離するために、Dependency Injection(DI)を使うことは多い。 その際に、一種類の依存するインタフェースにつき、一つだけインスタンスを渡せば良いのであれば話は単純なのだが、一つのインタフェースでも、コンストラクタの異なる複数のインスタンスを渡したい、というようなケースがある。
例えば、以下のようなケースである。
IHttpClient
(実装はHttpClient
) という、何らかのHTTPリクエストを送るためのクライアントがあるHttpClient
はコンストラクタの引数としてHTTP接続の際のベースURLを受け取るとする。- このクライアントを利用する
SampleController
では、2つの異なるベースURLに接続する必要があるため、それぞれコンストラクタの異なる2つのHttpClientのインスタンスを利用したい
以下のやり方で、このような場合にも対応できる。
Unity を使っている場合
DIのライブラリにUnityを使っている場合、Unityの持つ名前付きでRegisterTypeする機能を使う。
var BaseUriA = "a.example.com"; // 1つ目のベースUri var BaseUriB = "b.example.com"; // 2つ目のベースUri var container = new UnityContainer(); container.RegisterType<IHttpClient, HttpClient>( "HttpClientA", // 一つ目の引数として任意の名前を渡す new InjectionConstructor(BaseUriA) ); container.RegisterType<IHttpClient, HttpClient>( "HttpClientB", new InjectionConstructor(BaseUriB) ); // 上でつけた名前を使って、2つのHttpClientをSampleControllerのコンストラクタに渡す container.RegisterType<SampleController>(new InjectionFactory(c => new SampleController( c.Resolve<IHttpClient>("HttpClientA"), c.Resolve<IHttpClient>("HttpClientB") ) ));
public class SampleController : ApiController { private IHttpClient HttpClientA; private IHttpClient HttpClientB; public SampleController(IHttpClient HttpClientA, IHttpClient HttpClientB) { this.HttpClientA = HttpClientA; this.HttpClientB = HttpClientB; } }
Autofac を使っている場合
Unityはもはやメンテされていないため、今後新しいプロジェクトでは利用しないことが望ましい。代替としてよく名前の上がるAutofacの場合、Named and Keyed Services
という機能で目的の挙動が実現できる。
依存を受け取る側で、IIndex<K, V>という型で複数インスタンスを受け取り、名前を指定することで目的のインスタンスを取り出す。
var BaseUriA = "a.example.com"; # 1つ目のベースUri var BaseUriB = "b.example.com"; # 2つ目のベースUri var builder = new ContainerBuilder(); builder.RegisterType<HttpClient>() .WithParameter(new TypedParameter(typeof(string), BaseUriA)) .Named<IHttpClient>("HttpClientA"); builder.RegisterType<HttpClient>() .WithParameter(new TypedParameter(typeof(string), BaseUriB)) .Named<IHttpClient>("HttpClientB");
public class SampleController : ApiController { private IIndex<HttpClient, IHttpClient> httpClients; public SampleController(IIndex<HttpClient, IHttpClient> httpClients) { this.httpClients = httpClients; } public IHttpClient GetClientA() { return httpClients["HttpClientA"]; } }
参考
地味に便利な mkpasswd でランダムパスワードを生成する
いろいろな条件でパスワード生成できる mkpasswd
コマンドが地味に便利。
CentOS の場合デフォルトでは入っていないので、下記にてインストールする。
$ sudo yum install expect
オプションにて、文字数や記号・数字の数を指定できる。
オプション | 意味 |
---|---|
-l 数字 | パスワードの長さを指定(デフォルトは9) |
-d 数字 | パスワードに含める数字の数を指定(デフォルトは2) |
-c 数字 | パスワードに含めるアルファベット(小文字)の数を指定(デフォルトは2) |
-C 数字 | パスワードに含めるアルファベット(大文字)の数を指定(デフォルトは2) |
-s 数字 | パスワードに含める特殊文字(記号など)の数を指定(デフォルトは1) |
-2 | パスワードが、右手と左手で交互に入力される文字配列となる(QWERTYキーボードの場合) |
個人的によく使うのは、下記の「8文字・数字2桁・記号なし」。
$ mkpasswd -l 8 -d 2 -s 0
参考
.NET 関連の製品名 (.NET Core, ASP.NET, .NET Framework...) を整理する
.NET 関連の勉強をするにあたり、まずは基本の基本からということで、.NET 系の製品名を整理する。馴染みのない人間からすると、似たような名前が多く、非常にまぎらわしい。Web記事などを参考にしているので、誤った理解があるかもしれないが、気づいたら適宜修正するということで、取り急ぎメモ。
- .NET
- .NET Framework
- ASP.NET Web Forms
- ASP.NET MVC
- ASP.NET 4.6
- ASP.NET Core 1.0 / ASP.NET 5(旧名称)
- ASP.NET MVC 5
- ASP.NET 4.x 系に対応した最新の ASP.NET MVC フレームワーク
- ASP.NET MVC 6
- ASP.NET Core 1.0 に対応した最新の ASP.NET MVC フレームワーク