BlazorでSPAするぞ!(1) - 正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

1. Blazorとは

公式Docsでは以下のように書かれています。

Blazor is a framework for building interactive client-side web UI with .NET

「Blazorは、.NETでインタラクティブなクライアントサイドWebユーザーインターフェイスを構築するためのフレームワークです。」ってことですね。
エンジニア的には、抽象的なモヤモヤ感のある文章ですね。
具体的に出来ることを簡単に言うと、今までJavaScript(+各種JSフレームワーク)でやってた事を、.NET Core(つまりC#)で作ることができるよ、ってものです。
JavaScript(or TypeScript) + React」とか「JavaScript(or TypeScript) + Vue」とかで作ってたようなSPAなアプリケーションをBlazorで作ることができます。
UI要素はHTML・CSSで構築し、ロジック部分をC#で記述するようなイメージになります。
メリットとしては「サーバーサイドもクライアントサイドもC#で開発できる」ってことです。
そもそもサーバーサイドにおいて非.NETなエンジニアにとっては、メリットとはならないものですが、.NETディベロッパーにとってはクライアントサイドもC#で書けるというのは生産性とかメンテナンス性とかでメリットがあります。
従来のWeb開発の React/Redux/TypeScript/Flow/Vue/Angular/Babel... の技術たちと比べるとキワ物感がありますが(Blazorは.NETに絞られる技術になるので)、ASP.NET Core+BlazorでのSPA開発は、かなり生産性高そうという印象を持っています(まだPreviewだし、実開発してない段階での感想ですが)。

1.1. WebAssembly

C#コードがクライアントサイドで動作するというキーワードは、過去にもいくつかありましたね。。。

2020年5月現在においては、オープンな技術開発的には使えない技術達ですね(一部のtoBでは生き残ってると思いますが・・・)。

で、Blazorがクライアントサイド(ブラウザ)でC#コードの動作を実現している仕組みですが、「WebAssembly」になります。
WebAssemblyは C/C++・Rust・Go言語 なんかで作れるようになっていますが、BlazorによってC#でもWebAssemblyが作れるようになりました。
厳密には、Blazorの.NET Coreアセンブリを.NET WASMでWebAssemblyにビルドして実行するような形になります。

図にすると以下のような感じ。

f:id:daigo-knowlbo:20200524125611p:plain

BlazorとDOMとの間に矢印がありますが、Blazorの中でプログラムによって画面要素に変更があるとそれがDOMに伝えられて画面の更新が行われます。
Reactの仮想DOMと同様に、Blazorでは「render tree」と呼ばれるUI要素構造をメモリに展開し、変更の必要がある部分のみDOMに反映させる仕組みを持っています。

1.2. Blazor WebAssembly(Client-Side Blazor) と Blazor Server(Server-Side Blazor)

Blazorは上述のようにクライアントサイドでC#が動作する技術(Blazor WebAssembly)ですが、これ以外に同じ実装をサーバーサイドで動作させるBlazor Serverというものがあります。
Blazorコンポーネントをサーバーサイドで実行する仕組みのものになります。クライアントサイドはプレーンなHTML+JSでWebAssemblyは実行されません。
実行イメージは以下のような感じ。

f:id:daigo-knowlbo:20190501214537p:plain

ボタンのクリックなど「Blazor的に影響のある事象が発生する」と、Signal Rでサーバにリクエストが行われ、サーバサイド(ASP.NET Core)でBlazorが動作し、結果をクライアントに送り返す。結果に応じたDOMの更新がクライアントで行われます(Web Formのポストバックみたいな処理が裏側で自動的に行われます)。

※ この投稿では特別に記述がない限り Blazor WebAssembly を前提とした説明を行います。

2. 開発環境構築

開発環境はWindowsでもMacでも行けますが、ここではWindowsを前提とします。

2.1. .net Core 3インストール

以下から.NET Core 3.1 SDKをダウンロードしてインストールします。

https://dotnet.microsoft.com/download/dotnet-core/3.1

コマンドプロンプトでバージョンを確認。

C:>dotnet --version 3.1.300

※2020/5/24現在の最新は 3.1.300

### 2.2. Blazorテンプレートのインストール コマンドプロンプトで以下のコマンドを実行します。
(dotnet newコマンドに、Blazorのプロジェクトテンプレートがインストールされます)

# 正式リリース版で不要になりました。
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview6.19307.2

2.3. C# for Visual Studio Codeのインストール(Visual Studio Code利用の場合)

Visual Studio Codeを使う場合は、「C# for Visual Studio Code」をインストールしておきます。

https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp

### 2.4. Visual Studio Extensionインストール(Visual Studio 2019利用の場合) Visual Studio 2019を使う場合は、「BlazorのExtension」をインストールしておきます。
(Visual Studio 2019に、Blazorのプロジェクトテンプレートがインストールされます)

https://marketplace.visualstudio.com/items?itemName=aspnet.blazor

※本記事では Visual Studio Code を開発に利用します。(VS2019だと.razorのファイルの追加がUIから出来ない等Extensionが十分にPreview版に追い付いていない感があったので)

3. Blazorプロジェクト作成

では早速Blazorプロジェクトを作成します。

3.1. dotnet コマンド / 4つのプロジェクトテンプレート

VS Codeベースで進めるのでdotnetコマンドを使っていきます。

まずプロジェクトテンプレートの確認を。
dotnet new」でテンプレート一覧を確認すると以下のようにBlazor関連のテンプレートが4種類表示されます。

C:\>dotnet new
使用法: new [オプション]
...省略

Templates                                         Short Name            Language          Tags
-------------------------------------------------------------------------------------------------------------------------------
...省略
Blazor Server App                                 blazorserver             [C#]              Web/Blazor
Blazor WebAssembly App                            blazorwasm               [C#]              Web/Blazor/WebAssembly
...省略

それぞれ以下の通りです。

(1) Blazor Server App

Blazor ServerのASP.NET Coreプロジェクトテンプレートです。

dotnet new blazorserver -o BlazorServerApp1

作成されるファイル達↓↓↓

f:id:daigo-knowlbo:20200524124904p:plain

(2) Blazor WebAssembly App

Blazor WebAssemblyのプロジェクトテンプレートです。

dotnet new blazorwasm -o BlazorWasmApp1

作成されるファイル達↓↓↓

f:id:daigo-knowlbo:20200524124942p:plain

(3) Blazor WebAssembly App with hosted app

blazorwasmテンプレートに --hosted オプションを追加すると、Blazor WebAssembly + ASP.NET Core WebAPI構成のソリューションが作成されます。
Blazor WebAssemblyの通信先WebAPIサービスとしてのASP.NET Coreプロジェクトもセットで作成されるイメージです。

dotnet new blazorwasm --hosted -o BlazorWasmAppWithHosted1

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20200524125006p:plain

4. Hello Blazor WebAssembly

では、純粋にBlazorを試すために「Blazor WebAssembly App(blazorwasm)」でプロジェクトを作成します。

ますプロジェクトを作成。hello_blazorという名前にします。

dotnet new blazorwasm -o hello_blazor

4.1. Visual Studio Codeで開く

VS Codeの「File - Open Folder」メニューで 作成した hello_blazor フォルダを開きます。

f:id:daigo-knowlbo:20200524125049p:plain

4.2. ビルド + 実行する

ある程度の動作する実装がテンプレートで出力されているので、ひとまずそのままビルドします。
コマンドプロンプトでプロジェクトフォルダに移動し、「dotnet build」コマンドを実行します。

daigo@ardentred MINGW64 /c/workspace/hello_blazor
$ dotnet build
.NET Core 向け Microsoft (R) Build Engine バージョン 16.6.0+5ff7b0c9e
Copyright (C) Microsoft Corporation.All rights reserved.

  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  hello_blazor -> C:\workspace\hello_blazor\bin\Debug\netstandard2.1\hello_blazor.dll
  hello_blazor (Blazor output) -> C:\workspace\hello_blazor\bin\Debug\netstandard2.1\wwwroot

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:03.30

obj / binフォルダが作成され、その下にアセンブリ(.dll)が出力されました。

では以下のように dotnet run コマンドを実行します。

daigo@ardentred MINGW64 /c/workspace/hello_blazor
$ dotnet run
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\workspace\hello_blazor

Blazor自体はClient sideのWebAssembly(の上で動く.NETアセンブリ)ですが、http://localhost:5000 / https://localhost:5001 としてhello_blazorをホストするHttpサーバが立ち上がります。
ブラウザで http://localhost:5000 にアクセスすると、テンプレートで出力したサンプル実装が起動します。

f:id:daigo-knowlbo:20200524125116p:plain

Hello World以上の立派な実装がなされています。

ちなみにこのページを表示した時のHttpRequestの状況ですが、以下のようになっています。

f:id:daigo-knowlbo:20200524125133p:plain

よく見ると「blazor.webassembly.js」とか「dotnet.wasm」とか「hello_blazor.dll」とか「Microsoft.AspNetCore.Components.WebAssembly.dll」とか・・・.NETが動こうとしている事が見て取れると思います。

それから、左のサイドメニューから「+Counter」を選択すると、ボタンクリックでカウントアップが行われる画面に移動します。
「Click me」ボタンをクリックすると、Current Countが1ずつカウントアップしますが、この時サーバへのリクエストが飛んでいないことも確認できます。
(当然ながらクライアントサイドでC#コードが実行されているので、このようなロジック処理でサーバへのラウンドトリップは発生しない、という事です)

f:id:daigo-knowlbo:20200524125154p:plain

4.3. Hello Blazorに着手!

何もしないで http://localhost:5000 にアクセスした画面がすでに「Hello, World!」になっています。
これを「Hello Blazor」に改造していこうと思います。
Blazorが動作するベースの仕組みは色々ありますが、、、とりあえず このページは「Pages/index.razor」で実装されています。

# Pages/Index.razor #

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

実行画面と見比べると 大体想像できる感じ ですよね(HTMLタグが書かれていて、ASP.NET CoreのRazorを知っていれば @xx なRazor構文が書かれているな・・と)。
画面上にヘッダーやサイドメニューが付いてるのはレイアウト設定が効いているからです(ASP.NET Coreの _Layout.cshtml とかと同じような仕組みがBlazorにもあり、それが適用されています )。
先頭の「@page "/"」は後(後続の投稿)で詳細を説明しますが、ルーティング設定です。"/"なのでルートとしてこのIndex.razorがルーティング適用されています。

(1) プロパティ変数の利用

以下のような画面に改造したいと思います。

f:id:daigo-knowlbo:20200524125221p:plain

HTMLをべた書きすれば実現できるのですが、それではあまり意味がないので、無理やりC#なコードを使っていきます。

# Pages/Index.razor(Hello Blazor版 その1) #

@page "/"

<h1>@Message</h1>

@code {
  private string Message = "Hello Blazor";
}

「@Message」は、C#で定義した変数(プロパティ)をレンダリングします。ASP.NETのRazorと完全に一緒です。
「@code」は、このコンポーネント(Index.razor)のコードブロックを表します。コードブロックには、C#で.NETコードを記述することができます。
string型のMessage変数を定義し、"Hello Blazor"で初期化しているのは見れば分かるでしょう。

こんな感じでクライアントサイド実装をC#で行うことができます。

(2) イベントの使用

では、次に以下のような画面に改造します。
「get date and time」ボタンを用意し、クリックされたらすぐ上にテキストで現在日時を表示します。

f:id:daigo-knowlbo:20200524125242p:plain

# Pages/index.razor(Hello Blazor版 その2) #

@page "/"

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="GetDateTime" value="get date and time" />

@code {
  private string Message = "Hello Blazor";
  private string CurrentDateTime = "???";
  
  private void GetDateTime() {
    CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
  }
}
@CurrentDateTime

CurrentDateTime変数を用意し(初期値は ??? )、「CurrentDateTime:@CurrentDateTime」として画面に表示しています。

ボタン

「<input type="button" @onclick="GetDateTime" value="get date and timee" />」でボタンを配置しています。
「@onclick="GetDateTime"」がポイントで、onclickはHTML/JavaScriptにおけるinputタグのイベントと同様です。
これに対して GetDateTime() メソッドをバインディングしています(onclickに対してJavaScriptのFunctionをバインドするのと同じ感覚ですね)。
GetDateTime()では DateTime.Now.ToString() という.NETのC#コードにより CurrentDateTime 変数を変更しています。
CurrentDateTimeの変更を受けBlazor Runtimeは、対象箇所の再レンダリングを行います。

良い感じにプロパティとイベントをC#で実装し、HTML要素に反映することが出来ましたね。

※ 「@onclick="GetDateTime"」の@GetDateTime部分は将来的に「@プレフィックス」を不要とする方針のようです。 ← 正式版出で不要になりました。

4.4. CodeBehindを使う

上述のIndex.razorは、.razorファイルに UI要素 も コード も混ざった状態になってしまいました。
よくある話ですが、Blazorでも UIとコード は分離可能です。
「UI(razor定義)」と「コード(C#ページロジック)」を分離する方法は2つあります。
1つは「razor定義のpartialクラスを実装する方法」、もう1つは「定義したページクラスをrazorで継承する方法」です。

(1) razor定義のpartialクラスを実装する方法

以下がrazor定義です。
@codeセクションを定義せず、UI定義のみを行っています。

# Pages/Index.razor (razor定義のpartialクラスを実装する方法) #

@page "/"

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="GetDateTime" value="get date and time" />

次に、ページロジッククラスを定義するIndex.csファイルを用意します。
定義するクラス名はrazor定義のファイル名「Index」と同様にします。また、partialクラスとして定義することが重要です。
razor定義側で要求される仕様に合わせ、Message / CurrentDateTimeプロパティ、GetDateTime()メソッドを実装します。

# Pages/Index.cs (razor定義のpartialクラスを実装する方法) #

using System;

namespace hello_blazor.Pages
{
  public partial class Index
  {
    private string Message { get; set; } = "Hello Blazor";

    private string CurrentDateTime { get; set; }= "???";

    private void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}
何故、クラス名をIndexにして、partialクラスで定義し、privateメンバーを定義したのか?

「razor定義のpartialクラスを実装する方法」は上記に説明した通りですが、「何故~」について少し深堀したいと思います。
答えは、コーディングしたhello_blazorを「dotnet build」により生成した「hello_blazor.dll」を見ることで明確になります。
hello_blazor.dllを ILSpy で逆コンパイルした結果が以下です。

f:id:daigo-knowlbo:20200524125308p:plain

// hello_blazor.Pages.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;

[Route("/")]
public class Index : ComponentBase
{
    private string Message
    {
        get;
        set;
    } = "Hello Blazor";


    private string CurrentDateTime
    {
        get;
        set;
    } = "???";


    private void GetDateTime()
    {
        CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }

    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "h1");
        __builder.AddContent(1, Message);
        __builder.CloseElement();
        __builder.AddMarkupContent(2, "\r\n<br>\r\nCurrentDateTime:");
        __builder.AddContent(3, CurrentDateTime);
        __builder.AddMarkupContent(4, "\r\n<br>\r\n");
        __builder.OpenElement(5, "input");
        __builder.AddAttribute(6, "type", "button");
        __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)GetDateTime));
        __builder.AddAttribute(8, "value", "get date and time");
        __builder.CloseElement();
    }
}

Index.razor / Index.razor.cs がコンパイルされることで、「Microsoft.AspNetCore.Components.ComponentBase」クラスを継承した「Index」クラスとなりました(名前空間は hello_blazor.Pages)。

まず、前提として .razor 定義は「Microsoft.AspNetCore.Components.ComponentBaseを継承したクラス(名前空間・クラス名は、プロジェクト・フォルダ名・razorファイル名から決定)」にコンパイルされます。そして、HTMLタグ や @プレフィックス 等による定義は「BuildRenderTree()」メソッド内でC#コードによる要素構築に変換されます。
つまり、Index.razor.cs で上記名前空間・クラス名に合わせたクラスを partial で定義すれば、razorから自動生成されるクラスのパーシャルクラス定義とすることができます。
これが「class partial Index { }」がIndex.razorの分離コード定義となる仕組みです。
そして、「Message / CurrentDateTime / GetDateTime()」が private のスコープ定義で事足りた理由も同じところにあります。Index.razorから自動生成されたIndexクラスと、Index.razor.csで定義したIndexクラスとは、partialによりコード上分離した位置に定義しただけであり、コンパイル後の実体は同一だからです。

(2) 定義したページクラスをrazorで継承する方法

以下がrazor定義です。

# Pages/Index.razor (定義したページクラスをrazorで継承する方法) #

@page "/"
@inherits IndexBase

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="GetDateTime" value="get date and time" />

「@inherits IndexBase」により、このrazor定義はIndexBaseクラスを継承することを明示しています。

次に、ページロジッククラスを定義するIndexBase.csファイルを用意します。
定義するクラス名は、razorファイル定義内の@inheritsでも指定した通り「IndexBase」です。
ページクラス(厳密にはBlazorのコンポーネントクラス)は「Microsoft.AspNetCore.Components.ComponentBase」を継承して作成します。
razor定義側で要求される仕様に合わせ、Message / CurrentDateTimeプロパティ、GetDateTime()メソッドを実装します。

# Pages/Index.cs (定義したページクラスをrazorで継承する方法) #

using System;
using Microsoft.AspNetCore.Components;

namespace hello_blazor.Pages
{
  public class IndexBase : ComponentBase
  {
    protected string Message { get; set; } = "Hello Blazor";

    protected string CurrentDateTime { get; set; }= "???";

    protected void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}
@inherits、partialでなくなったページクラス定義、プロパティ・メソッドはprotectedになった

partialクラスによるUIとロジックの分離実装からいくつかの要素に変更が加わっています。
再度、コンパイルにより得られた「hello_blazor.dll」を見ると根柢の仕組みが見えてきます。
先ほどと同様に ILSpy を使ってみましょう。

f:id:daigo-knowlbo:20200524125326p:plain

// hello_blazor.Pages.Index
using hello_blazor.Pages;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;

[Route("/")]
public class Index : IndexBase
{
    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "h1");
        __builder.AddContent(1, base.Message);
        __builder.CloseElement();
        __builder.AddMarkupContent(2, "\r\n<br>\r\nCurrentDateTime:");
        __builder.AddContent(3, base.CurrentDateTime);
        __builder.AddMarkupContent(4, "\r\n<br>\r\n");
        __builder.OpenElement(5, "input");
        __builder.AddAttribute(6, "type", "button");
        __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)base.GetDateTime));
        __builder.AddAttribute(8, "value", "get date and time");
        __builder.CloseElement();
    }
}

// hello_blazor.Pages.IndexBase
using Microsoft.AspNetCore.Components;
using System;

public class IndexBase : ComponentBase
{
    protected string Message
    {
        get;
        set;
    } = "Hello Blazor";


    protected string CurrentDateTime
    {
        get;
        set;
    } = "???";


    protected void GetDateTime()
    {
        CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
}

IndexクラスとIndexBaseクラスが別々に定義されています。そして、IndexはIndexBaseを継承しています。
これは実装した通りですが、IndexBaseクラスはpartialではなくComponentBaseクラスを継承した独立定義したクラスとして実装しました(IndexBase.cs)。
そして Index.razor では、「@inherits IndexBase」と明示的にIndexBaseクラスを継承する定義を行いました。

「.razorによりUI定義を行った結果生成されたIndexクラス」と「UIロジック実装として用意したIndexBaseクラス」は継承関係にあることから、「Message / CurrentDateTimeプロパティ、GetDateTime()メソッド」は privateスコープでは不足、「protected」以上のスコープが必要となります。

アーキテクチャ的には MVVM や Flux/Redux で実装ような選択肢もあるので、コードを @code{} に書くとか コードビハインドに書くとか がベストプラクティスどうかは、プロジェクトごとの選択になります。

5. まとめ

という感じで、Blazorではじめの一歩が踏み出せました!

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ! - 目次 -正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

Blazor(主にWebAssembly)についての基本をブログにまとめました。

2019年のGWにBlazorに興奮して初校を書きましたが、
2020年5月、遂に Blazor WebAssembly が正式リリースとなったので、改めて正式リリース版に合わせた記述に改訂しました。

2019年GWを使って Blazor についての基本をブログにまとめました。

本投稿は「目次」になります。

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

Github から Azure DevOps にRepositoryを移行する

1. はじめに

最近、再び自分の中のAzure熱が高まったので Github に置いているPrivate Repository(C#)をAzure DevOpsに引っ越ししてみました。

ソース管理としてGithubは素晴らしいのですが、Azure DevOpsもCI/CD含めた統合環境として(特に.NETプロジェクトでは)素晴らしいようなのでちょっと遊びとして使ってみたいなぁ、というのが移行の動機です(あ、勿論Githubでも外部サービス含め、同様のCI/CD環境は得られるけど)。

という事で、ここではサンプルとして用意したGithub RepositoryをAzure DevOpsに移行してみます。

2. 移行元のGithub

移行元のGithub Repositoryは、Private の https://github.com/ryuichi111/ToAzureDevOpsMigrateTest.git というHelloWorldな.NET Coreコンソールアプリケーションを想定します。
SourceTreeで確認した状態は以下の通り。

f:id:daigo-knowlbo:20190201003756p:plain

当該Repositoryに対しては、以下のような操作が行われた状態です。

  • message="init commit" として最初のソースをmasterにコミット/プッシュ
  • message="modified message" としてHelloWorldのコンソール出力文字を変更したソースをmasterにコミット/プッシュ
  • feature/interactiveブランチを切ってソース修正コミット/プッシュし、masterへのPRによりmerge

3. Github -> Azure DevOpsに移行する

3.1. プロジェクト作成

Azure DevOpsのホーム画面を表示します。
右上の「Create project」をクリック。

f:id:daigo-knowlbo:20190201002357p:plain

Project nameに「ToAzureDevOpsMigrateTest」と入力、Visibilityは「Private」、Version Controlは「Git」、Work item processは「Agile(まぁ、これはお好みで)」にします。
「Create」ボタンをクリックします。

f:id:daigo-knowlbo:20190201002457p:plain

3.2. Githubからインポート

プロジェクトが作成出来たら、左サイドバーから「Repos」をクリックします。
さらに、画面中央少し下の「import」をクリックします。

f:id:daigo-knowlbo:20190201002609p:plain

「Import a Git repository」ウィンドウが表示されるので、移行元のURLを「Clone URL」に、また、Username / Passwordを入力します。

f:id:daigo-knowlbo:20190201011125p:plain

3.3. GithubからPersonal Access Tokenを取得(二要素認証有効時のみ)

が・・・Githubで二要素認証を有効化している場合はパスワードで認証が通らないので、Github上で以下の操作を行って Personal Access Token を取得し、それを「Pasword / PAT」のテキストボックスに入力します。

Githubサイトに行き、「setting -> Developer settings -> Personal access tokens -> Generate new token」を選択します。

f:id:daigo-knowlbo:20190201003246p:plain

任意の説明を「Toke Description」に付け、repoへのスコープを付けて、画面下にある「Generate token」ボタンをクリックします(以下のキャプチャでは下のボタンは映っていませんが)。

f:id:daigo-knowlbo:20190201002946p:plain

トークンが生成されたのでコピーします。

f:id:daigo-knowlbo:20190201003429p:plain

Azure DevOps側の「Password / PAT」にトークンをペーストして「Import」ボタンをクリックします。

3.3. クローン中。。。

f:id:daigo-knowlbo:20190201003553p:plain

Github -> Azure DevOpsへのCloneが走って・・・

f:id:daigo-knowlbo:20190201003611p:plain

3.4. Azure DevOps上のRepositoryを確認

Azure DevOpsのRepo上でHistoryを確認してみましょう。

f:id:daigo-knowlbo:20190201003707p:plain

Commit / PR mergeの履歴含めてきれいに移行出来ました。

4. まとめ

GithubがMS傘下になり、これまでGithubに置いていた資産をAzure DevOpsに移行しても、なんだか変な安心感を覚えるように個人的にはなりました^^;;;
まぁ、少なくとも.NETプロジェクトに関してはAzure DevOpsに乗っかっていた方がCI/CDなど含め諸々のメリットが享受できそうな気がしています。
一昔前のMSであればお金を払わないと何もできなかった感じですが、最近のMSならこのAzure DevOpsに関しても個人としての入り口部分では無料でいろいろできますし。

ということで、僕はプライベートではAzure DevOpsを楽しんでいこうかと思います!(仕事はGithubだからいい感じにバランス取れてるでしょ)

ASP.NET Core 2.2 + JWTのサンプル(AccessToken/RefreshToken利用)

ASP.NET Core 2.2(WebAPI) と JWTを使った、認証付きWebAPIの実装を↓↓↓に置きました。

github.com

雑実装ですがAccessTokenとRefreshTokenに対応しています。

※説明ブログは後程。。。書く予定。。。

EntityFramework Core 2.2 + Cosmos DB ~ ファーストステップ

1. はじめに

2018年10月(?)あたりからPreview版とはいえ、EntityFramework CoreからCosmos DBにアクセスするプロバイダが提供されていたという事で試してみました。
データの保存と読み込みを行うだけの超基本となるファーストステップの記事になります。

使用した環境

2. こんな事をするよ

すごく単純に EF Core Cosmos DB Provider を使って単純なモデルクラスの保存と読み込みを行います。

3. Cosmos DBの作成

Azureポータルの「+リソースの作成」からCosmos DBを作成します。

f:id:daigo-knowlbo:20190108015649p:plain

f:id:daigo-knowlbo:20190108015756p:plain

4. 実装

以下順に。

4.1. プロジェクト作成

新規プロジェクトを作成します。
コンソール アプリ(.NET Core)

f:id:daigo-knowlbo:20190108012847p:plain

プロジェクト名は「EfCoreCosmosExamConsole」

f:id:daigo-knowlbo:20190108012915p:plain

4.2. Nuget追加

Nugetパッケージで「microsoft.EntityFrameworkcore.Cosmos」を追加します。
プレリリース版を含めるにして検索してインストールします。

f:id:daigo-knowlbo:20190108013743p:plain

4.3. モデルクラス / DbContextクラス追加

Cosmos DBに保存するデータモデルクラスを追加します。
FamilyクラスとPersonクラスとします(家族情報を保存するイメージ)。
※手抜きして Models/Family.cs の1ファイルに2つのクラスを定義しています。

// Models/Family.cs
using System;
using System.Collections.Generic;

namespace EFCoreCosmosExamConsole.Models
{
  public class Family
  {
    public Guid FamilyId { get; set; }

    public Person HeadOfHousehold { get; set; }

    public Person Partner { get; set; }

    public List<Person> Children { get; set; }
  }

  public class Person
  {
    public Guid PersonId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime Birth { get; set; }
  }
}

FamilyクラスがPersonクラスを世帯主(HeadOfHousehold )、パートナー(Partner)、子供(Children)として保有しています。

DbContextクラスを作成します。
RDBに対するEF Coreの時とほぼ同様の感じです。

// Models/PeopleContext.cs
using Microsoft.EntityFrameworkCore;

namespace EFCoreCosmosExamConsole.Models
{
  public class PeopleContext : DbContext
  {
    public DbSet<Family> Families { get; set; }

    public DbSet<Person> Persons { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseCosmos(
        "https://ryuichi111cosmos.documents.azure.com:443/",
        "ひみつひみつひみつひみつひみつひみつひみつひみつ",
        "PeopleDatabase"
      );
    }
  }
}

Cosmos DBプロバイダ固有の設定は「OnConfiguring()」での「UseCosmos()」呼び出しになります。
UseCosmos()メソッドは、Microsoft.EntityFrameworkCore.CosmosアセンブリMicrosoft.EntityFrameworkCore.CosmosDbContextOptionsの拡張メソッドとして定義/実装されています。
第1引数には接続先Cosmos DBのURL、第2引数には接続キー、第3引数には(任意の)データベース名を指定します。
「第1引数 接続先Cosmos DBのURL」「第2引数 接続キー」はAzureポータルの対象Cosmos DBのKeysで確認することができます。

f:id:daigo-knowlbo:20190108014227p:plain

ここまでで、ソリューションエクスプローラ的には↓↓↓こんな感じになる。

f:id:daigo-knowlbo:20190108013030p:plain

4.4. main()からDbContext呼び出し

Program.cs main()にCosmos DBへのデータ保存と読み込みを記述します。
RDBに対するEF Core実装とほぼ同じです。

// Program.cs
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using EFCoreCosmosExamConsole.Models;

namespace EFCoreCosmosExamConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      // テスト用の家族オブジェクトを作成(世帯主+パートナー+子供×2)
      Person takashi = new Person() { PersonId = Guid.NewGuid(), FirstName = "Takashi", LastName = "Tanaka", Birth = new DateTime(1985, 4, 5) };
      Person sawako = new Person() { PersonId = Guid.NewGuid(), FirstName = "Sawako", LastName = "Tanaka", Birth = new DateTime(1983, 7, 12) };
      Person chiyori = new Person() { PersonId = Guid.NewGuid(), FirstName = "Chiyori", LastName = "Tanaka", Birth = new DateTime(2001, 10, 4) };
      Person mamoru = new Person() { PersonId = Guid.NewGuid(), FirstName = "Mamoru", LastName = "Tanaka", Birth = new DateTime(2002, 11, 20) };
      Family family = new Family()
      {
        FamilyId = Guid.NewGuid(),
        HeadOfHousehold = takashi,
        Partner = sawako,
      };
      family.Children = new List<Person>();
      family.Children.Add(chiyori);
      family.Children.Add(mamoru);

      // CosmosDBに保存
      using (var context = new PeopleContext())
      {
        // Database / Collectionを(無ければ)作成
        context.Database.EnsureCreated();

        // Familyをコンテキストに追加
        context.Families.Add(family);

        // SaveChanges()の裏側でオブジェクトをJSON変換、シャドウプロパティの追加、CosmosDBへの保存、が行われる
        context.SaveChanges();
      }

      // CosmosDBから読み込み
      using (var context = new PeopleContext())
      {
        // Familyを関連オブジェクトごと取得
        var loladedFamily = context.Families
          .Include(f => f.HeadOfHousehold)
          .Include(f => f.Partner)
          .Include(f => f.Children)
          .Where(f => f.FamilyId == family.FamilyId).FirstOrDefault();

        // Personを単独で取得
        var loadedSawako = context.Persons
          .Where(p => p.PersonId == sawako.PersonId).FirstOrDefault();
      }
    }
  }
}

テスト用に Family / Person モデルクラスの作成

Cosmos DBに保存するテスト用のモデルクラスはべた書きで作成しています。

Cosmos DBの初期化

AzureポータルからCosmos DBの入れ物は作成済みですが、それに紐づく「データベース」「コレクション」がまだ作成されていません。
以下の呼び出しを行うと「データベース」「コレクション」が存在しなければ作成してくれます。

context.Database.EnsureCreated();

ちなみにEnsureCreated()呼び出し直後に、Azureポータル Data Explorer で確認した状態は以下です。

f:id:daigo-knowlbo:20190108020029p:plain

データベース名=PeopleDatabase(PeopleContextにおいてUseCosmos()の第3引数で指定した名称)
コレクション名=PeopleContext(PeopleContextのクラス名)
Throughput(RU) = 400

Familyオブジェクトの保存

以下の呼び出しでCosmos DBにデータを保存することができます(RDBに対するEF Coreと全く同じ)。

context.Families.Add(family);
context.SaveChanges();

以下のように、同一のコレクション(PeopleContext)内に Family / Person オブジェクトが保存されます。

↓↓↓Family f:id:daigo-knowlbo:20190108020510p:plain

↓↓↓Person f:id:daigo-knowlbo:20190108020536p:plain

↓↓↓Person f:id:daigo-knowlbo:20190108020601p:plain

EF Core側の実装における DbContextの括りでコレクションが作成され、そこに対象DbContextが扱うオブジェクトが保存されます。
Cosmos DBにおいては「コレクション=RUの括り(=コスト/パフォーマンス)」であるため、実運用上はコストとパフォーマンスの兼ね合いでDbContextのくくりを検討することになると思います。

それから Family / Person クラスで定義していないプロパティが多数Cosmos DB側のJsonデータには含まれていますが、これらはCosmos DB側で保持するシャドウプロパティになります。

Familyオブジェクト / Personオブジェクトの読み込み

データの読み込みもRDBに対するEF Coreと全く同様です。

// Familyを関連オブジェクトごと取得
var loladedFamily = context.Families
  .Include(f => f.HeadOfHousehold)
  .Include(f => f.Partner)
  .Include(f => f.Children)
  .Where(f => f.FamilyId == family.FamilyId).FirstOrDefault();

// Personを単独で取得
var loadedSawako = context.Persons
  .Where(p => p.PersonId == sawako.PersonId).FirstOrDefault();

まとめ

EF Core Cosmos DB Providerは、RDB操作と非常に類似した(同様の)コードでCosmos DBへのアクセスが可能でいい感じですね。
かつてLINQがデータソースに依存しない(オンメモリオブジェクトであろうがRDBデータであろうが)プログラミングモデルを目指し、実現しましたが、そんなテイストで 相手がRDBであろうがCosmos DBであろうが同様のプログラムコードが書けるのはうれしいです。
もちろんアーキテクチャ的にはバックエンドの技術知識を持つ必要がありますが、すごく期待できるデータプロバイダな気がしました^^

サンプルコードは一応↓↓↓↓↓です。 github.com

ASP.NET Core 2.2 WebAPI で Pagination対応 する

1. はじめに

ASP.NET Core WebAPIにおいて、pagination(ページング)でJSONデータを返す実装のメモです。
(ググれば既出だけど、意外に情報少なめだったので、自分メモの意味も込めて)

開発環境

※ VS2017でもCore2.xでも同じだと思う。

2. こんな実装をする

pagination対応するときは、主に以下の2つがあります。

  • body要素のjsonにページ番号や全体件数を含ませる
  • body要素には本来のデータのみを含ませ、HTTP Response Headerにページ情報や全体件数を含ませる

ここでは後者の実装を行います。

社員情報をpaginationで5件ずつ取得することができるAPIを想定します。

GET /api/employee?page=10

↑↑↑とすると
↓↓↓が帰るみたいな

[
  {
    "id": 45,
    "firstName": "しゃいん",
    "lastName": "45号"
  },
  {
    "id": 46,
    "firstName": "しゃいん",
    "lastName": "46号"
  },
  {
    "id": 47,
    "firstName": "しゃいん",
    "lastName": "47号"
  },
  {
    "id": 48,
    "firstName": "しゃいん",
    "lastName": "48号"
  },
  {
    "id": 49,
    "firstName": "しゃいん",
    "lastName": "49号"
  }
]

3. 実装コード

注意)以下のコードは、paginationに関する部分のみに集中したコードです。DBもEFもクラスモデリングも無視でなるべく簡易な実装にしているので、クラス構成・メソッドの抽出等々は無視したコードです。

3.1. モデルクラス Employee。

// Models/Employee.cs
namespace PaginationExam.Models
{
  public class Employee
  {
    public int ID { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }
  }
}

3.2. Daoクラス

Employeeを取得するDaoクラス。(ダミーリストデータをメモリ内に作ってそれをLINQでpaginationして返すだけの実装)

// Dao/EmployeeDao.cs
using System.Linq;
using System.Collections.Generic;
using PaginationExam.Models;
using System;

namespace PaginationExam.Dao
{
  public class EmployeeDao
  {
    private List<Employee> _dummyEmployeeData = new List<Employee>();

    public EmployeeDao()
    {
      for (int i = 0; i < 100; i++)
      {
        this._dummyEmployeeData.Add(
          new Employee()
          {
            ID = i,
            FirstName = "しゃいん",
            LastName = i.ToString() + "号"
          });
      }
    }

    public (int totalItemCount, int lastPage, List<Employee>) GetEmployees(int page, int countPerPage)
    {
      int totalItemCount = 0;
      int lastPage = 0;
      List<Employee> employees = null;

      employees = this._dummyEmployeeData.Skip(countPerPage * (page - 1)).Take(countPerPage).ToList();
      totalItemCount = this._dummyEmployeeData.Count();

      lastPage = (int)Math.Floor((decimal)totalItemCount / countPerPage);
      if (totalItemCount % countPerPage > 0)
        lastPage++;

      return (totalItemCount, lastPage, employees);
    }
  }
}

3.3. コントローラクラス

Web API のコントローラクラス。 「GET api/Employee?page=xx」を受け付けます。
1ページ当たり5件は固定です。
pagination用のHTTP Response Headerは以下の3つを追加しています。

Links

以下の4つのリンク先を示します。
first - 1ページ目のURI prev - 前のページのURI
next - 次のページのURI
last - 最後のページのURI

X-TotalItemCount

「X-」なのでカスタムヘッダです。
Employeeの全件数を「X-TotalItemCount」に設定しています。

X-CurrentPage

現在のページを設定しています。

※「X-」カスタムヘッダは非推奨とされましたが、個人的にはその経緯から使ってOKじゃね?との認識。

// Controllers/EmployeeController.cs
using Microsoft.AspNetCore.Mvc;
using PaginationExam.Dao;
using PaginationExam.Models;
using System.Collections.Generic;

namespace PaginationExam.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  public class EmployeeController : Controller
  {
    // 1ページに5件
    const int CountPerPage = 5;

    [HttpGet]
    public IEnumerable<Employee> GetList([FromQuery] int page)
    {
      if (page < 1) // 1ページスタートなので、1未満は1に簡単に補正
        page = 1;

      // データ取得
      // 全件数、該当ページのEmployeeリスト
      var dao = new EmployeeDao();
      (int totalItemCount, int lastPage, List<Employee> employees) = 
        dao.GetEmployees(page, EmployeeController.CountPerPage);

      // Response Header追加
      this.Response.Headers.Add("Links", this.CreateLinksHeader("Employee", page, lastPage));
      this.Response.Headers.Add("X-TotalItemCount", totalItemCount.ToString());
      this.Response.Headers.Add("X-CurrentPage", page.ToString());

      // Body Jsonは本来のデータのみ
      return employees;
    }

    /// <summary>
    /// Pagination用のLinkヘッダ値を作成
    /// </summary>
    /// <param name="controller"></param>
    /// <param name="currentPage"></param>
    /// <param name="lastPage"></param>
    /// <returns></returns>
    protected string CreateLinksHeader(string controller, int currentPage, int lastPage)
    {
      List<string> links = new List<string>();

      links.Add(string.Format("<{0}>; rel=\"first\"", this.Url.Link("", new { Controller = controller, page = 1 })));
      if (currentPage > 1)
      {
        links.Add(string.Format("<{0}>; rel=\"prev\"", this.Url.Link("", new { Controller = controller, page = currentPage - 1 })));
      }
      if (currentPage < lastPage)
      {
        links.Add(string.Format("<{0}>; rel=\"next\"", this.Url.Link("", new { Controller = controller, page = currentPage + 1 })));
      }
      links.Add(string.Format("<{0}>; rel=\"last\"", this.Url.Link("", new { Controller = controller, page = lastPage })));

      return string.Join(", ", links);
    }
  }
}

4. 実行

実行します。
Postmanを起動して「GET https://localhost:44332/api/employee?page=10」をSendした結果は以下です。

BodyのJSONには本来のデータ(Employeeリスト)のみ。

f:id:daigo-knowlbo:20190106124027p:plain

Response Headerには Link / X-TotalItemCount / X-CurrentPage が返却されている。

f:id:daigo-knowlbo:20190106124106p:plain

5. まとめ

ASP.NET Core 2.2 でのpaginationについての一実装例でしたm( )m
ソースは以下に置いてあります。

github.com

あと、paginationに関する説明や検討事項は以下なんかが結構参考になると思います。

  1. 翻訳: WebAPI 設計のベストプラクティス - Qiita

  2. qiita.com

  3. RFC 5988 - Web Linking

OpenAPI Generator + golang + Flutter でアプリ開発

1. はじめに

以下のような構成のスマホアプリを作ってみようと思います。

f:id:daigo-knowlbo:20181103213543p:plain

一般的な、HTTP経由でサーバと通信するタイプのスマホアプリです。

で、タイトルの通り「OpenAPI 3.0でWebAPIを定義」し「golangでWebAPIを実装」し「Flutterでスマホアプリを実装」する、ということをしたいと思います。
また、OpenAPI 3.0定義からサーバ及びクライアントコードを自動生成するために OpenAPI Generator を使用することとします。

github.com

今回利用した環境  

macOS Mojave 10.14
Flutter 0.8.2
go 1.10.3
openapi-generator 3.3.2

2. 準備

Flutter / go 環境は構築済みの前提とします。
openapi-generatorは以下のbrewコマンドでインストールします。

$ brew install openapi-generator

3. 作る!

大まかな流れは以下になります。

  • OpenAPI 3.0 specでAPI仕様を定義
  • OpenAPI Generatorでgolangのserverコードを自動生成
  • OpenAPI Generatorでdartのclientコードを自動生成
  • Flutterアプリを作成(dart clientを利用してgolang serverを呼び出す)

では順番に進めていこうと思います。
まずは、アプリ開発用のディレクトリを作成します。

$ mkdir oas3_go_flutter_exam
$ cd oas3_go_flutter_exam

3.1. OpenAPI 3.0 specを作成

OpenAPI定義yamlを作成します。

ファイル名は「employee_api.yml」とします。
idを指定してemployee情報を取得するWebAPIの定義になります。
Employeeは id / firstName / lastName / salary の属性を持ったオブジェクトとします。

openapi: "3.0.0"
info:
  version: 1.0.0
  title: oas3_go_flutter_exam Employee
  license:
    name: MIT
servers:
  - url: http://localhost:8080/api/
paths:
  /employee:
    get:
      summary: get employee information
      operationId: getEmployee
      tags:
        - employee
      parameters:
        - name: employeeId
          in: query
          description: query target employee id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: return employee information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Employee"
components:
  schemas:
    Employee:
      required:
        - id
        - firstName
        - lastName
        - salary
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        salary:
          type: integer
          format: int64

3.2. yamlの妥当性をチェック

定義した employee_api.yml にエラーがないか、以下のコマンドでチェックします。

$ openapi-generator validate -i employee_api.yml

Validating spec (employee_api.yml)
No validation issues detected.

3.3. golangのserverコードを自動生成

以下のコマンドでgolangのserverコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g go-server -o ./server

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/model_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/api_employee.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/api/openapi.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/main.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/Dockerfile
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/routers.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/logger.go
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/go/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./server/.openapi-generator/VERSION

モデルクラス(model_employee.go)やルート定義(routers.go)、APIのカラ定義(api_employee.go)など一通りのgo実装が自動生成されました。

3.4. goのWebAPIを実装

自動生成されたコードにオリジナルの実装を追加します。

api_employee.goファイル の GetEmployee() に Employeeオブジェクト を返却する実装を以下のように追加します。
(本来はDBアクセスなどを行った結果を返すと思います)

package openapi

import (
    "encoding/json"
    "net/http"
)

// GetEmployee - get employee information
func GetEmployee(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    w.WriteHeader(http.StatusOK)

    employeeID := r.URL.Query()["employeeId"]

    employee := Employee{
        Id:        employeeID[0],
        FirstName: "ryuichi " + employeeID[0],
        LastName:  "daigo " + employeeID[0],
        Salary:    12000000}
    json.NewEncoder(w).Encode(employee)
}

3.5. go serverを実行

WebAPIがうまく動作するか実行してみましょう。

$ go run main.go

curlでもブラウザでも何でも良いですが、以下のURLにGETを投げてみます。

http://localhost:8080/api/employee?employeeId=10

{"id":"10","firstName":"ryuichi 10","lastName":"daigo 10","salary":12000000}

golang serverのWebAPIが正しく動作していることを確認できました。

3.6. dartのclientコードを自動生成

以下のコマンドでdartのclientコードを自動生成します。

$ openapi-generator generate -i employee_api.yml -g dart -DbrowserClient=false -o ./client

[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/model/employee.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/Employee.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/lib/api/employee_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/docs/EmployeeApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/daigo/Projects/oas3_go_flutter_exam/./client/.openapi-generator/VERSION

※ 上記のように「-DbrowserClient=false」オプションを付けないとFlutterでコンパイルできない dart:html に依存したコードが生成されてしまうので注意。

dartによりWebAPI呼び出しを行うコード、また送受信するデータのモデルクラスなど一通りの実装が自動生成されました。

3.7. Flutterアプリを作成

以下のコマンドでFlutterアプリを作成します。

 $ flutter create flutter_app

先程自動生成した dart client を利用して golang server にアクセスするために pubspec.yamldart client への依存定義を追加します。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

  openapi:
    path: ../client/

※ 上記定義の下2行

main.dartを修正します。
IDを入力し、ボタンを押すとWebAPI呼び出しが行われ、結果を画面にテキスト表示するようにします。

// main.dart

import 'package:flutter/material.dart';
import 'package:openapi/api.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter with OpenAPI Generator'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  String _employeeName = '';
  String _employeeId = '';

  void _callWebApi() {    
    var client = new EmployeeApi();
    var result = client.getEmployee(this._employeeId);
    result.then(
      (employee) => setState(() { this._employeeName = employee.firstName + ' ' + employee.lastName; } )
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text('pleas input employee id'),
            new TextField(
              onChanged: (v) => this._employeeId = v,
            ),
            new RaisedButton(
              child: new Text('call WebAPI'),
              onPressed: _callWebApi,
            ),
            new Text(
              this._employeeName,
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
    );
  }
}

3.8. 実行

では実行してみます。

./serverディレクトリで以下のコマンドによりgolang serverを起動しておきます。

go run main.go

そしてFlutterアプリを起動します。

f:id:daigo-knowlbo:20181103211906p:plain

IDを入力し、call WebAPIボタンをタップします。

f:id:daigo-knowlbo:20181103212648p:plain

WebAPI呼び出しが行われEmployeeオブジェクトの取得&表示が行われました!

4. まとめ

OpenAPI Generator を使うとモデルを含めたテンプレート実装が一瞬のうちに生成されかなりいい感じに開発が進められるんじゃないかと思いました。サクサクっと必要な定形コードが自動生成されるので、ドメイン領域への集中力アップにも勿論効果的ですね。
OpenAPI 3.0によるAPI定義を明確に行った上で、サービスを実装していくスタイルも(まあSwagger時代から何年も行われていることではありますが)クリーンにプロジェクトが保たれていいと思います。

今回作ったコードは以下に置いておきました。

github.com