.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。

www.microsoft.com

Visual Studio 2017 Preview のインストール

.NET Core 2.1 を使うには、Preview 版の Visual Studio が必要となる。以下のリンクからインストールする。

www.visualstudio.com

なお Preview 版は、通常版の Visual Studio 2017 と同居させられる作りになっている。通常版を利用している人も何も考えずにインストールしてしまって大丈夫。 同居させると、インストーラの画面はこんな感じになる。

f:id:nosunosu:20180518120526p:plain

テンプレートからプロジェクトを新規作成

プロジェクトの新規作成で、「ASP.NET Core Web アプリケーション」から、「Blazor (ASP.NET Core hosted)」を選択する。

今回は「Blazor (ASP.NET Core hosted)」を選択して、サーバサイドの Web API を含んだソリューションを作成するが、すでに Web API が別にあり、純粋にクライアントサイドのみ開発したい場合には、クライアントアプリのみのプロジェクトが生成される「Blazor」を選択する。

f:id:nosunosu:20180518120553p:plain

※「Blazor」を選択した場合、ビルドして生成されるのは単なる静的ファイル群なので、静的な Web サイトをホストできるサービスであればどこにでも配置して公開できる。一方「Blazor (ASP.NET Core hosted)」の場合は、当然ながら ASP.NET Core が動く環境でである必要がある。

なおいずれの場合も、ランタイムは ASP.NET Core 2.1 または 2.0 を選択すること。

プロジェクトを作成すると、以下のようなプロジェクト構成のソリューションが作成される。

f:id:nosunosu:20180518120649p:plain

  • [プロジェクト名].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 というプロジェクトがすでに存在していた。

github.com

Redux 風の APIC# で実装していて、Redux を知っていればすぐに使えそう。 ちゃんとブラウザ上で Redux DevTools を使って State を見られるようにしてあるようでえらい。

で、こういうのが必要かどうかっていうのは結局 JS のフロント開発と一緒で、開発規模がかなり大きくなれば、必要になってくるんだろうとは思う。

参考

learn-blazor.com

codedaze.io

Microsoft Graph(Microsoftクラウドサービスの統合API)の使い方

Microsoft Graph を使うと、OneDrive (for Business) や SharePoint, Outlook, Teams 等を含む多くの Microsoftクラウド製品を、標準化された REST API 経由で操作できる。 developer.microsoft.com

何ができるかはの詳細は上記リンク先に記載されているが、以下のようなリソースの主要な操作はだいたい行うことができる。

今回は例として、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 となるので、メモしておく。

f:id:nosunosu:20180626121314p:plain

1.2 Client Secret の生成

アプリ作成後には、メニューからキーを生成しておく。これが後ほど用いるClient Secretとなるので、同じくメモしておく。

f:id:nosunosu:20180626121325p:plain

1.3 アプリへの権限付与

さらに、作成したアプリに対して、Graph で利用したいリソースへのアクセス許可を付与する。 アプリの「設定」から「必要なアクセス許可」→「追加」と進み、API として「Microsoft Graph」を、アクセス許可としては「アプリケーションのアクセス許可」から「Read all groups」を選択して保存する。

f:id:nosunosu:20180626121338p:plain

そのうえで、「必要なアクセス許可」の画面から再度「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 例外ハンドリング

GraphServiceClientServiceException 型の例外を投げる。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 IDClient 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

参考

任意の文字数でパスワードをランダム生成するには - @IT

.NET 関連の製品名 (.NET Core, ASP.NET, .NET Framework...) を整理する

.NET 関連の勉強をするにあたり、まずは基本の基本からということで、.NET 系の製品名を整理する。馴染みのない人間からすると、似たような名前が多く、非常にまぎらわしい。Web記事などを参考にしているので、誤った理解があるかもしれないが、気づいたら適宜修正するということで、取り急ぎメモ。

参考

エディタのVimモードで、Escキーを押したときに日本語入力をオフにする設定(macOS, Windowsそれぞれの場合)

VimgVim、あるいはAtomやVS Codeなどの、Vimバインドモードを利用できるエディタで日本語を扱う際に、問題となるのが日本語入力モードの切り替えだ。デフォルトだと、日本語入力モードでEscやCtrl+[を押してノーマルモードに抜けた際に、日本語入力状態のままとなってしまい、コマンド操作ができなくなってしまう。

そこで、

  • Vim、またはAtomやらVS CodeやらVisual StudioなどのVimバインドモードを利用できるエディタを使っている際には
  • EscまたはCtrl+[を押下した際に
  • 日本語入力をOFFにする

ように設定を行う。

続きを読む