goa v2.0.0-wip 事始め

1. golangはじめました(趣味で)

3〜4週間前から趣味でgolangを始めてみました。
ということでやっと基礎学習も出来つつあり、色々試し始められたので、たまにブログっていければと思います。
唐突ですが goa というものに興味を持って使ってみたので、その辺のお話を取り上げたいと思います。

また、本投稿の動作確認に使った環境は以下のとおりです。
* macOS High Sierra 10.13.6
* go 1.10.3
* $GOPATH=/Users/daigo/go

2. goaとは

goaとは、APIを作る際 DSLによりデザインファイルを記述すると そこから自動で実装の雛形やドキュメント(swaggerファイル)を生成してくれるマイクロサービス用フレームワークです。
私もそうだったのですが、DSLって聞くと、また新しい記法を覚えなくちゃいけないのかぁ・・・と思いますが、goaのDSLgolangで記述することができるので、敷居はだいぶ低くなっていると思います。まあ、golangによるgoaの記法を覚える必要はありますが(汗
ということで、goaよりも以前からある swagger からの流れと同じく、以下のような問題の解決を図るフレームワークとなります。

  • いきなりAPI書くんじゃなくてきちんと設計しようよ
  • でもドキュメント(設計書)書くのめんどいよね(トラディショナルカンパニーのExcel設計書とかクソだよね)
  • バックエンドAPI開発者とフロントエンド開発者が分離したりしてくると仕様の齟齬は避けたいよね
  • ドキュメント書いてもコードのバージョンアップにきちんと追随したいよね(労力最小限で)

v1とv2

golang本体もそうですが、周辺フレームワークのバージョンアップもまだまだかなり激しいですよね。
goaのバージョンは2018/8/19時点で、「stableバージョン=v1.4.0」「次のメジャーバージョン=v2.0.0-wip」です。
v2はまだwipな状態ですが、github上で以下のように記述されている通り、既に結構作り込まれているのではないかと思います。

goa v2 is currently in beta: it is robust enough to be used in production but there may still be breaking changes before the final release. If you're new to goa then you may want to consider starting with v2.

v1.x系とv2.0ではDSLの記法、ツールコマンドの使い方が変わっているので改めて学習する必要があります。とはいえ、向かっている思想は同じなのでスムーズに移行できるのではないかと思います。(既存コードのMigrationは難しい?その観点では調べてないので?はてな?です)

goaのgihhubは以下(v1.x)で↓↓↓

github.com

その中にv2ブランチが切られています。↓↓↓

github.com

3. goa v2をインストール

goa v2のインストールは以下のコマンドでいけます。

$ go get -u goa.design/goa/...

インストールの確認は「goa version」コマンドで行えます。

$ goa version
goa version v2.0.0-wip

4. goa v2を使ってみる

公式github上のドキュメントトップでも説明されているレベルの「はじめてのgoa v2」的なサンプルを作っていこうと思います。
サンプル仕様はDBもセッションも使わず完全ステートレスなWebAPIとします。

ガチャサービスとして以下の2つのメソッドを実装します。
* ガチャを回す(GETリクエスト、URLパラメータでガチャタイプをIntで指定)
 → GET http://localhost:8080/{type}
 → ガチャ結果はStringで返却
* チートガチャを回す(POSTリクエスト)
 → POST http://localhost:8080/ POSTデータ=secret_key(string)
 → ガチャ結果はStringで返却

4.1. プロジェクトの作成

gachaプロジェクトを作成します。
(ひとまずdepも使わないし、ディレクトリ掘るだけ)

$ cd $GOPATH/src
$ mkdir gacha
$ cd gacha

結局、この段階で ↓ にいる感じです。

$ pwd
/Users/daigo/go/src/helloGoa/gacha

4.2. designファイルの定義

goaではdesignファイルを用意して、そこにgolangAPI仕様を記述します。

$ mkdir design
$ vi design/design.go
// design/design.go

package design

import . "goa.design/goa/http/design"
import . "goa.design/goa/http/dsl"

var _ = API("gacha", func() {
    Title("gacha service")
    Description("サンプルのガチャサービスです。")
    Server("http://localhost:8080")
})

var _ = Service("gachasvc", func() {
    Method("pon", func() {
        Description("ガチャを回します")
        Payload(func() {
            Attribute("type", Int, "gacha type")
        })
        Result(String)
        HTTP(func() {
            GET("/pon/{type}")
        })
    })

    Method("cheat_pon", func() {
        Description("チートガチャを回します")
        Payload(func() {
            Attribute("secret_key", String, "秘密の鍵")
        })
        Result(String)
        HTTP(func() {
            POST("/pon/cheat/")
        })
    })
})

API

APIはトップレベDSLです。
APIのタイトルやホスト名バージョン等の記述を行います。
指定できる仕様の詳細は公式Docにありますが、ここでは省略します。

Service / Method

サービスDSLは、メソッドを定義します。
なんとなく眺めると理解できると思いますが、「pon」と「cheart_pon」の2つのメソッドが定義されています。
descriptionは、そのままメソッドの説明ですね。
Payloadは、メソッドが受け取るペイロード(引数)の定義です。サンプルではそれぞれのメソッドが1つづつ受け取っていますが、複数の場合はAttribute()定義を並べて定義します。
Resultは、メソッドの結果の戻り値の型定義になります。
HTTPは、メソッドをHTTPでホストすること、そしてHTTP Verb(GET/POST等)とルーティングURLを定義しています。
Payload/ResultとHTTPが分離されているところが重要で、「メソッド名/INの引数/OUTの戻り値の定義」と@トランスポートのプロトコル」は分離して考えることが出来ます。例えば、HTTPではなくgRPCでホストする定義に簡単に書き換えることが出来ます(今日段階ではgRPCはgoa v2で対応していない?)。

4.3. コード自動生成

では、goaのDSL定義(design.go)からコードの自動生成を行います。
「goa gen gacha/design」コマンドを実行します。
gacha/designの部分は $GOPATH/src からの相対パスでdesign.goが定義されているパスを指定します。

$ goa gen gacha/design                                                                                                               [~/go/src/gacha]

gen/gachasvc/client.go
gen/gachasvc/endpoints.go
gen/gachasvc/service.go
gen/http/cli/cli.go
gen/http/gachasvc/client/cli.go
gen/http/gachasvc/client/client.go
gen/http/gachasvc/client/encode_decode.go
gen/http/gachasvc/client/paths.go
gen/http/gachasvc/client/types.go
gen/http/gachasvc/server/encode_decode.go
gen/http/gachasvc/server/paths.go
gen/http/gachasvc/server/server.go
gen/http/gachasvc/server/types.go
gen/http/openapi.json
gen/http/openapi.yaml

15ファイルほど自動生成されました。
サービスエンドポイントやHTTPトランスポートレイヤの実装などが自動生成されています。
swaggerファイル(openapi.json / openapi.yaml)も自動生成されています。

openapi.jsonは以下のような定義になっています。

# 自動生成された openapi.json
{
  "swagger": "2.0",
  "info": {
    "title": "gacha service",
    "description": "サンプルのガチャサービスです。",
    "version": ""
  },
  "host": "localhost:8080",
  "paths": {
    "/pon/cheat": {
      "post": {
        "tags": [
          "gachasvc"
        ],
        "summary": "cheat_pon gachasvc",
        "description": "チートガチャを回します",
        "operationId": "gachasvc#cheat_pon",
        "parameters": [
          {
            "name": "CheatPonRequestBody",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/GachasvcCheatPonRequestBody"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK response.",
            "schema": {
              "type": "string"
            }
          }
        },
        "schemes": [
          "http"
        ]
      }
    },
    "/pon/{type}": {
      "get": {
        "tags": [
          "gachasvc"
        ],
        "summary": "pon gachasvc",
        "description": "ガチャを回します",
        "operationId": "gachasvc#pon",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "description": "ガチャタイプ",
            "required": true,
            "type": "integer"
          }
        ],
        "responses": {
          "200": {
            "description": "OK response.",
            "schema": {
              "type": "string"
            }
          }
        },
        "schemes": [
          "http"
        ]
      }
    }
  },
  "definitions": {
    "GachasvcCheatPonRequestBody": {
      "title": "GachasvcCheatPonRequestBody",
      "type": "object",
      "properties": {
        "secret_key": {
          "type": "string",
          "description": "秘密の鍵",
          "example": "Qui iste laboriosam quis ducimus fugiat qui."
        }
      },
      "example": {
        "secret_key": "Accusamus iste."
      }
    }
  }
}

「goa gen」コマンドではサービス自体のドメインロジックの実装部や、サーバとしてホストするロジックが生成されていません。
これらを実装する出発点となる実装の生成は「goa example gacha/design」コマンドで行うことが出来ます。

$ goa example gacha/design

cmd/gacha_cli/main.go
cmd/gacha_svc/main.go
gachasvc.go

3つのgoファイルが生成されました。

cmd/gacha_cli/main.go

APIメソッドを呼び出すテスト用CLIコードです。

cmd/gacha_svc/main.go

HTTPでサービスをホストする実装コードです。ログ出力等含めた実装になるので、ここからカスタマイズして自分の実装に持っていってもいいと思います。

gachasvc.go

ガチャサービスの実装雛形です。 ここに自分のドメインロジックの実装を行います。

// 自動生成された gachasvc.go

package gacha

import (
    "context"
    gachasvc "gacha/gen/gachasvc"
    "log"
)

// gachasvc service example implementation.
// The example methods log the requests and return zero values.
type gachasvcSvc struct {
    logger *log.Logger
}

// NewGachasvc returns the gachasvc service implementation.
func NewGachasvc(logger *log.Logger) gachasvc.Service {
    return &gachasvcSvc{logger}
}

// ガチャを回します
func (s *gachasvcSvc) Pon(ctx context.Context, p *gachasvc.PonPayload) (res string, err error) {
    s.logger.Print("gachasvc.pon")
    return
}

// チートガチャを回します
func (s *gachasvcSvc) CheatPon(ctx context.Context, p *gachasvc.CheatPonPayload) (res string, err error) {
    s.logger.Print("gachasvc.cheat_pon")
    return
}

上記の Pon() / CheatPoin() の中身を実装すればいいですね。
自動生成されたコードでは、ログ出力と空文字のreturnが実装されています。

4.4. ドメインロジックの実装

では、Pon() / CheatPoin()の2つのメソッドに対する、面白みのない実装は以下のとおりです。
(ベタに読めるように、べた書きしています。)
乱数を発生させて確率で「SS激レア・Sレア・レア」の結果文字列を返却しています。
パラメータは *gachasvc.PonPayload ポインタ型として引数で受け取れるので * p.Type みたいに参照することが出来ます。(ポインタなので * を忘れずに)

// gachasvc.goに実装を追加

// ガチャを回します
func (s *gachasvcSvc) Pon(ctx context.Context, p *gachasvc.PonPayload) (res string, err error) {
    s.logger.Print("gachasvc.pon")

    rand.Seed(time.Now().UnixNano())
    r := rand.Intn(10)

    if *p.Type == 0 { // ノーマルガチャ
        if r == 9 {
            return "SS激レア", nil
        } else if r > 6 {
            return "Sレア", nil
        }
    } else if *p.Type == 1 { // 高確率ガチャ
        if r == 7 {
            return "SS激レア", nil
        } else if r > 3 {
            return "Sレア", nil
        }
    }
    return "レア", nil
}

// チートガチャを回します
func (s *gachasvcSvc) CheatPon(ctx context.Context, p *gachasvc.CheatPonPayload) (res string, err error) {
    s.logger.Print("gachasvc.cheat_pon")

    if *p.SecretKey == "神の手" {
        return "SS激レア", nil
    }

    // 不正チート。垢BAN処理!!!!

    return "ノーマル", nil
}

4.5. ビルド&実行

ではビルドして実行しましょう。
まず、プロジェクトルート($GOPATH/src/gacha)で「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha
$ go build

サーバ(サービス)の実行

次にサーバ(サービス)実装をビルドします。
$GOPATH/src/gacha/cmd/gacha_svcディレクトリで「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha/cmd/gacha_svc
$ go build

lsすると gacha_svc が生成されたのを確認できます。

$ ls -la 
total 19528
drwxr-xr-x  4 daigo  staff      128  8 19 12:58 .
drwxr-xr-x  4 daigo  staff      128  8 19 12:01 ..
-rwxr-xr-x  1 daigo  staff  9990876  8 19 13:10 gacha_svc
-rw-r--r--  1 daigo  staff     4073  8 19 12:01 main.go

サービスを実行します。

$ ./gacha_svc

[gacha] 13:13:09 method "Pon" mounted on GET /pon/{type}
[gacha] 13:13:09 method "CheatPon" mounted on POST /pon/cheat
[gacha] 13:13:09 listening on :8080

OK。8080ポートでリスニングしてくれています。

クライント(CLI)の実行

ブラウザやPostmanなどからサービスを叩くことも出来ますが、goa example がサーバ(サービス)を叩くクライントCLIも自動生成してくれています。
もう1つターミナルを開いて、「$GOPATH/src/gacha/cmd/gacha_cliディレクトリで「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha/cmd/gacha_cli
$ go build

lsすると gacha_cli が生成されたのを確認できます。

$ ls -la 
total 18000
drwxr-xr-x  4 daigo  staff      128  8 19 12:58 .
drwxr-xr-x  4 daigo  staff      128  8 19 12:01 ..
-rwxr-xr-x  1 daigo  staff  9211836  8 19 13:17 gacha_cli
-rw-r--r--  1 daigo  staff     2285  8 19 12:01 main.go

「./gacha_cli --help」で使い方が表示されます。

./gacha_cli is a command line client for the gacha API.

Usage:
    ./gacha_cli [-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags]

    -url URL:    specify service URL (http://localhost:8080)
    -timeout:    maximum number of seconds to wait for response (30)
    -verbose|-v: print request and response details (false)

Commands:
    gachasvc (pon|cheat-pon)
    
Additional help:
    ./gacha_cli SERVICE [ENDPOINT] --help

Example:
    ./gacha_cli gachasvc pon --type 2753046654881133159

/pon/{type}への GET リクエストは以下の通り。

$ ./gacha_cli gachasvc pon --type 0

"Sレア"

/pon/cheat/への POST リクエストは以下の通り。

$ ./gacha_cli gachasvc cheat-pon --body '{
     "secret_key": "神の手"         
  }'
 
"SS激レア"

5. まとめ

ということで goa の基本的な利用の流れの紹介でした。
記事中では十分に紹介しきれていませんが、改めてgoaを使うメリットは以下のようなものがあります。

  • なるべく少ないコストで、API設計ドキュメントを作成・メンテする事ができる(goa DSL -> swagger doc)
  • APIメソッドの入力パラメータ仕様に基づいてgoa側(+生成されたコード)がバリデーション+パラメータエンティティオブジェクトへの割当を行ってくれる
  • つまり開発者は、ドメイン領域へのコーディングに集中することができる

ということで、本ブログでは初めてgolang関連を取り上げましたが、私の所属会社ではメインのRubyに続いてgolangが最近元気なようなので、引き続きgolangに(趣味で)取り組んで行こうと思うので引き続きブログっていきたいと思います。

(Durable Functions)「第22回 Azureもくもく会」に参加+LTした話

久しぶりに kingkino@マンダム (@kingkinoko) on Twitterさん主催のAzureもくもく会に参加しました。
で、LTもさせていただきました。

1. 何を もくもく+LT したの?

「Durable Functionsの基礎学習」とその発表でした。
(何かを作り上げた!みたいなものではないのだけれど、昔から技術のバックグラウンドを調べたりするの好きなので)

今年に入ってから、仕事では .NET/Azure を離れ、Java/AWS の世界に移っていましたが、ぽろぽろと趣味で.NET/Azureは やっていました。
で、先日のGlobal Azure Boot Camp 2018に参加してAzure界隈の方々とお話しさせていただいたら「Durable Functions」がホットだということで。
GW終わり辺りからドキュメントを読み、もくもく+LTをさせていただいたという感じです。

2. 発表資料

発表資料は↓↓↓です。

speakerdeck.com

なのですが、「学んだこと」「伝えたかった事」のメインはDemoをさせていただいた部分でした。
加えて、「内容がLT・プレゼン向きじゃない」+「私の説明が決して上手くない」ということで、伝わりにくかったかと思います。

ということでDemoした部分について、ブログらせて頂こうかと・・・

3. 何を学んだのか?何をDemoしたのか?

MS公式のDurable Functionsの資料を読んだ結果、以下のドキュメントがDurable Functionsが Durable である所以的な部分を指しているような気がしました。

docs.microsoft.com

しかし、
「ドキュメントに記述されているルールに従って実装すれば、いい感じにDurable Functionsは動くのだろうけど、何故上手く動くのかが腑に落ちない」
という感覚を持ち、その もやもや を払拭するための技術探索を行いました。
つまり、以下のような内容が、話としては理解するけれど、具体的にそうなる根拠となるバックエンドの知識が欲しい、と。。。

・オーケストレーター関数
 決定論的である必要がある。
 複数回実行されても結果が同じでなければならない。
  →現在日付の取得・GUIDのランダム生成の実行はNG。
  →DateTime.Now ではなく DurableOrchestrationContext.​Current​Utc​Date​Time を使う
・アクティビティ関数
 非決定論的操作が可能。
 ある責務を持ったドメイン ファンクションはここで定義。
  →つまり「DBアクセス」などの処理はアクティビティ関数に実装
・イベントソーシング(パターン)で実装されている。
・Azure Storage(キュー・テーブル)に実行履歴をチェックポイントする事で信頼性を得ている。
・awaitが呼び出されるとオーケストレーター関数をゼロから再実行する。

4. Demo内容

Durable Functionsがどのようなフローで実行されるのか?Azure Storageを使ってどのようにDurableに実行されるのか?(直訳すれば "丈夫に" "恒久的に"ですね)
ほぼVSテンプレが吐き出す以下のコードで検証します(Http TriggerによるDurable Functionsコード)。
カスタムしたのは 19 / 20行目 を追加したぐらいでしょうか。

01: using System;
02: using System.Collections.Generic;
03: using System.Net.Http;
04: using System.Threading.Tasks;
05: using Microsoft.Azure.WebJobs;
06: using Microsoft.Azure.WebJobs.Extensions.Http;
07: using Microsoft.Azure.WebJobs.Host;
08: 
09: namespace FunctionApp2
10: {
11:   public static class Function2
12:   {
13:     [FunctionName("Function2")]
14:     public static async Task<List<string>> RunOrchestrator(
15:       [OrchestrationTrigger] DurableOrchestrationContext context)
16:     {
17:       var outputs = new List<string>();
18: 
19:       var currentUtc = context.CurrentUtcDateTime;
20:       var current = DateTime.Now; // ※本当はオーケストレーター関数で実行しちゃいけないやつ
21: 
22:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Tokyo"));
23: 
24:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Seattle"));
25: 
26:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "London"));
27: 
28:       return outputs;
29:     }
30: 
31:     [FunctionName("Function2_Hello")]
32:     public static string SayHello([ActivityTrigger] string name, TraceWriter log)
33:     {
34:       log.Info($"Saying hello to {name}.");
35:       return $"Hello {name}!";
36:     }
37: 
38:     [FunctionName("Function2_HttpStart")]
39:     public static async Task<HttpResponseMessage> HttpStart(
40:       [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
41:       [OrchestrationClient]DurableOrchestrationClient starter,
42:       TraceWriter log)
43:     {
44:       // Function input comes from the request content.
45:       string instanceId = await starter.StartNewAsync("Function2", null);
46: 
47:       log.Info($"Started orchestration with ID = '{instanceId}'.");
48: 
49:       return starter.CreateCheckStatusResponse(req, instanceId);
50:     }
51:   }

ブレークポイントは張りまくります。

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

テストではローカルAzure Storage(Emu)を使いますが、中身を完全に空にしておきます。

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

4.1. 実行

(1) デバッグ実行

プロジェクトをデバッグ実行します。
以下のような感じでローカルでAzure Functionsが実行されます。

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

ここで、Azure Storage ExplorerでStorageの確認を行います。

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

「Blob Contaners」「Queues」「Tables」に色々作成されています(中身は空)。

※上記Storage項目のそれぞれの説明はここらへんに詳細が書かれています。構成により調整が可能。

(2) HTTPトリガーをキック

HTTPトリガーの受け口である「http://localhost:7071/api/Function2_HttpStart」をポストマンでキックします。

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

当然、FunctionsのHTTPトリガーである「HttpStart()」が呼び出されます。

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

「続行(F5)」を行います。
すると、コードの実装の通り「StartNewAsync("Function2")」でFunction2オーケストレーター関数が呼び出されます。
ということで、以下の画面のように Function2オーケストレーター関数 でブレークします。

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

再び「続行(F5)」して「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」まで飛んだ状態が以下です。

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

日付取得結果は以下の通りでした。

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:30:46

今度は「ステップオーバー(F10)」します。
CallActivityAsync()により Function2_Hello が呼び出され、以下の場所でブレークします。

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

この状態でAzure Storage Explorerにて「DurableFunctionsHubHistoryテーブル」を確認すると以下のようレコードが作成されています。

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

※要するに「Function2オーケストレーター関数→Function2_Helloアクティビティ関数の呼び出しの履歴の保存」がAzure Storageに対して行われたということです。

で、「続行(F5)」しちゃいます。
結果、ブレークするのはココ↓↓↓↓↓。

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

そう、次のアクティビティ関数呼び出しの個所ではなく、オーケストレーター関数の頭のブレークに飛びます。

また、「続行(F5)」しちゃいましょう。
こんな感じになりますね↓↓↓。

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

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:32:31

DateTime.Nowは実行した時間そのものが取得されているのに対し、context.CurrentUtcDateTime値は「初回実行時と同じ値」が取得されています!
何度実行しても(何度再生されても)結果が同じ、つまり「決定論的動作」をしています。オーケストレーター関数の条件を満たしている!

次に「ステップオーバー(F10)」すると、先程1度実行済みの「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」が実行されるはず・・・
では「ステップオーバー(F10)」してみます。
Function2_Helloアクティビティ関数は呼び出されず、次のアクティビティ関数呼び出し箇所「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Seattle"));」に移りました。

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

さらに「ステップオーバー(F10)」してみます。
今度は、Function2_Helloアクティビティ関数が呼び出されました(引数nameはもちろんSeattle)。

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

ここで再びAzure Storage Explorerを見てみましょう。
レコードが増えましたね。Function2_Helloアクティビティが2回呼ばれた記録が行われています。

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

また、Result列に「Hello Tokyo!」と、1つ目のアクティビティ関数の処理結果が保存されています。

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

ここでちょっといたずら的にTokyoをOsakaにUpdateしてしまいます。

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

再び「続行(F5)」しちゃいましょう。
想像通り、オーケストレーター関数のトップに帰ってきます。

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

F5を連打し、Durable Functionsの処理をすべて終了させます。

結果として処理の履歴はDurableFunctionsHubHistoryテーブルに以下のように出力されました。

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

DurableFunctionsHubInstancesテーブルを見ると、処理結果が保存されています。

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

ポストマンでstatusQueryGetUriをリクエストしてみると・・・

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

先程いたずらでAzure Storageデータを変更した「Osaka」の文字が。
つまり、ロジックをメモリ上で実行して終わりではなく、Azure Storageに処理状態を永続化していることが理解できました。

5. まとめ

長々と分かりにくい検証を行いましたが以下のことが明確に理解できたのではないかと思います。

  • Durable Functionsは、アクティビティ関数の実行履歴を細かくAzure Storageに保存している。
  • Durable Functionsは、オーケストレーター関数で定義されたアクティビティ関数を高い信頼性で実行するためにawaitのタイミングでAzure Storageに状態を保存し、自らをリプレイしてすべてのアクティビティ関数をワークフローとして実行している。

Durable Functionsの実装については以下のgithubで公開されているので、更にソースレベルで探索ができるのではないかと思います。

github.com

また、勉強会等でもkingkino@マンダム (@kingkinoko) on Twitterさんや(「🍖・ω・)「🍺 (@yu_ka1984) on Twitterさん などDurable Functionsマスターがいらっしゃるので、色々伺えるのではないかと思います。

Syncfusion SfCalendarを使う(Xamarin Forms)

1. はじめに

Xamarin Formsで、Syncfusion SfCalendarを使って以下のようなサンプル実装を行いました。

f:id:daigo-knowlbo:20180328013715p:plain:w300
f:id:daigo-knowlbo:20180328020349p:plain:w300

  • 月カレンダー形式で予定を表示する
  • 日をクリックすると、対象日のスケジュール詳細(件名)が表示される
  • スワイプもしくはボタンクリックで前後の月に移動できる
  • Prismを使ってMVVMアーキテクチャとする

2. 実装手順

2.1. VS 2017でプロジェクト作成

Visual Studio 2017でPrism Blank App(Xamarin.Forms)プロジェクトを作成。
プロジェクト名は「UseSfCalendarWithPrism」としました。

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

2.2. Nugetパッケージ管理でSyncfusion SfCalendarを追加

XamarinNugetパッケージ管理で「Syncfusion.Xamarin.SfCalendar」をインストール。

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

2.3. 実装

で、実装は以下に置きました。。。。

github.com

以下にサンプル実装のポイントを・・・

ポイント1 カレンダーの月変更イベントをViewModelにバインド

カレンダーの月変更イベントは SfCalendar.MonthChanged です。
MVVMとしているのでイベントをViewMode(MainPageViewModel)で受け取りたいです。
その為、Prismの「EventToCommandBehavior」を利用して、イベントをView→ViewModelにコマンドとして伝播させています。

[MainPage.xaml]
<ContentPage ...省略
                        xmlns:b="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" />
<SfCal:SfCalendar x:Name="calendar" 
                  ShowInlineEvents="True"
                  MinDate="{Binding MinDate}"
                  MaxDate="{Binding MaxDate}"
                  BlackoutDates="{Binding BlackoutDates}"
                  DataSource="{Binding CalendarEventCollection}"
                  SelectionMode="{Binding SelectionMode, Mode=OneWay,Converter={StaticResource SelectionModeConverter}}">
    <SfCal:SfCalendar.Behaviors>
        <b:EventToCommandBehavior EventName="MonthChanged" 
                              Command="{Binding MonthChangedCommand}"
                              EventArgsParameterPath="args.CurrentValue" />
    </SfCal:SfCalendar.Behaviors>
</SfCal:SfCalendar>
[MainPageViewModel.cs]

public class MainPageViewModel : ViewModelBase
{
  ...省略
  public ICommand MonthChangedCommand => new Command<DateTime>((currentDate) =>
  {
    this.UpdateEvents(currentDate);
  });
}

ポイント2 カレンダーにバインドするイベントコレクション作成

SfCalendarに表示するイベントは「SfCalendar.DataSourceプロパティ」に設定しますが、PrismでViewModelにバインドしているので「MainPageViewModel.CalendarEventCollectionプロパティ」にデータバインドしています。
カレンダーの月変更イベントに対して当該月+前後一週間のイベントをバインドデータに設定しています。

[MainPageViewModel.cs]
  public class MainPageViewModel : ViewModelBase
  {
    
    /// <summary>
    /// イベントを更新します。
    /// </summary>
    /// <remarks>
    /// 今月のイベントを表示するために、対象月+前後一週間のイベントをコレクションに設定します。
    /// 1ヶ月のカレンダーの前後に 前月・次月 の日付が表示されるため、前後1週間のイベントを設定します。
    /// </remarks>
    /// <param name="calendarDate"></param>
    private void UpdateEvents(DateTime calendarDate)
    {
      // バインド対象のthis.CalendarEventCollectionを直接、繰り返しAdd()するとパフォーマンスが著しく落ちるのでテンポラリにデータコレクションを用意して差し替える
      CalendarEventCollection newCalendarEventCollection = new CalendarEventCollection();

      DateTime dt = new DateTime(calendarDate.Year, calendarDate.Month, 1);
      int thisMonthLastDay = dt.AddMonths(1).AddDays(-1).Day;
      for (int i = -7; i < thisMonthLastDay+7; i++)
      { // イベントはサンプルなので適当に2日に1回散歩と仕事、毎日のランチを設定
        DateTime eventDt = dt.AddDays(i);

        if (eventDt.Day % 2 == 1)
        {
          //this.calendarEventCollection.Add(
          newCalendarEventCollection.Add(
          new CalendarInlineEvent()
          {
            Subject = $"{eventDt.Day}日 散歩",
            StartTime = eventDt.AddHours(10),
            EndTime = eventDt.AddHours(11),
            Color = Color.Green
          });
        }
        //this.calendarEventCollection.Add(
        newCalendarEventCollection.Add(
          new CalendarInlineEvent()
          {
            Subject = $"{eventDt.Day}日 ランチ",
            StartTime = eventDt.AddHours(12),
            EndTime = eventDt.AddHours(13),
            Color = Color.Orange
          });
        if(eventDt.Day % 2 == 0)
        {
          //this.calendarEventCollection.Add(
          newCalendarEventCollection.Add(
            new CalendarInlineEvent()
            {
              Subject = $"{eventDt.Day}日 仕事",
              StartTime = eventDt.AddHours(10),
              EndTime = eventDt.AddHours(19),
              Color = Color.Blue
            });
        }
      }
      this.CalendarEventCollection.Clear();
      this.CalendarEventCollection = newCalendarEventCollection;
    }
  }

つまずいた点

2018/3/28現在版 Syncfusion SfCalendar(android)に不具合がありました。
イベントを設定しているのに画面上で、対象日をクリックすると「No Appointments」と表示されてしまう不具合でした。
twitterでつぶやいたところSyncfusionの方よりリプライを頂き、即座にパッチアッセンブリを頂き不具合の修正を確認できました。
アップデートとしては2018年3月末の「2018 Vol 1 SP1」で対応されるとのことです。

SfCalendarへの要望

表示形式が「YearViewとMonthView」の2つなのですが、Weeklyとかandroidのカレンダーみたいな形式とか、色々なバリエーションがあればもっと嬉しいなぁ、と思いました。

※今日のブログ、雑ですね。。。まあ、動くソースをgithubに置いたので、「おかしいぞ!分からんぞ!」と思った方はコメント頂ければと思いますm( )m

Xamarin FormsでCarouselViewControlを使う

1. はじめに

Xamarin Formsで以下のようなUIを作りたくて、Carouselコントロールについて あたふた したのでブログにメモっておきます。

f:id:daigo-knowlbo:20180322003924p:plain:w300 f:id:daigo-knowlbo:20180322004001p:plain:w300 f:id:daigo-knowlbo:20180322004009p:plain:w300

2. いくつかのCarousel実装

久しぶりにXamarin Formsアプリ作り始めたのですが、そんな自分にとってはカルーセルがカオスに見えました。。。
どれ 使えばいいの?と・・・
ググったら公式のと非公式のと色々出てきまして、今回は「alexrainman/CarouselView」を使用しました。
一応公式の2つと合わせて以下の3つについて書いておこうと思います。

  • Xamarin.Forms..CarouselPage
  • Xamarin.Forms.CarouselView
  • alexrainman/CarouselView

2.1. Xamarin.Forms.CarouselPage

Xamarin Formsの標準クラスです。
名前からも分かるようにページクラスです。
クラス図でいうと以下のような感じ。なので、ページ内の1要素として配置することは出来ないですね。

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

2.2. Xamarin.Forms.CarouselView

公式から出された奴ですね。
なんか一時期、CarouselPageはdeprecatedしてCarouselViewに移行する的な話を聞いた気がするのですが、Nugetしようとしたら、2016/7/28のプレリリースで止まっている。。。これはタヒってるのかな。。。

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

2.3. alexrainman/CarouselView

と、上記公式の2つが使えそうにないので、ググったら良さげなものがありました。

「alexrainman/CarouselView(https://github.com/alexrainman/CarouselView)」

こちらは以下のようにXamarin.Forms.Viewクラスの派生クラスとして実装されているのでPage内の1要素として利用することができます。
※「alexrainman/CarouselView」の実際に配置するコントロールクラス名は CarouselViewControlクラス になります。

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

3. 実装

では実装します。
(超簡単なサンプルだけど、alexrainman/CarouselViewを使った説明は日本語に少なげだったので、書いておこうと思いました。)
あと、一応 Prism の上で実装します。

3.1. ソリューション・プロジェクトの作成

Visual Studio 2017を起動します。
メニュー「ファイル→新規作成→プロジェクト」を選択。
ここでは、以下の設定で作成します。

プロジェクトテンプレート:「Prism→Xamarin.Forms→Prism Blank App(Xamarin.Forms)」
名前:UseAlexCarouselViewApp

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

ターゲットは android / iOS としました。

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

3.2. Nugetパッケージの追加

UseAlexCarouselViewAppプロジェクト(Formsの共通実装のプロジェクト)のNugetパッケージマネージャを表示して、CarouselView.FormsPluginを検索し、インストールします(これが alexrainman/CarouselView になります)。

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

3.3. モデルクラスの追加

せっかくPrismを使用しているので、ページ情報を表すモデルクラスを追加します。
UseAlexCarouselViewApp/Models/PageInfo.cs を追加します。

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

// UseAlexCarouselViewApp\Models\PageInfo.cs

using Xamarin.Forms;

namespace UseAlexCarouselViewApp.Models
{
  public class PageInfo
  {
    public string Name { get; set; }
    public Color ForeColor { get; set; }
    public Color BackColor { get; set; }
  }
}

3.4. ViewModelにPageInfoコレクションを追加

デフォルトで作られたMainPageViewModelクラスにPageInfoコレクションプロパティを追加します。
後でPageに配置するCarouselViewControlのページにバインドするプロパティになります。

// UseAlexCarouselViewApp\ViewModels\MainPageViewModel.cs

using System.Collections.ObjectModel;
using Prism.Navigation;
using Xamarin.Forms;
using BlankApp6.ViewModels;

namespace UseAlexCarouselViewApp.ViewModels
{
  public class MainPageViewModel : ViewModelBase
  {
    // CarouselViewControlにバインドするページ情報コレクション
    public ObservableCollection<PageInfo> CarouselPageInfo { get; set; }

    public MainPageViewModel(INavigationService navigationService) 
      : base (navigationService)
    {
      Title = "Main Page";

      // コレクション初期化
      this.CarouselPageInfo = new ObservableCollection<PageInfo>
      {
        new PageInfo
        {
          Name = "Page1",
          ForeColor=Color.Yellow,
          BackColor= Color.Red
        },
        new PageInfo
        {
          Name = "Page2",
          ForeColor=Color.Black,
          BackColor= Color.Yellow
        },
        new PageInfo
        {
          Name = "Page3",
          ForeColor=Color.Gold,
          BackColor= Color.Green
        }
      };
    }
  }
}

3.5. PageにCarouselViewControlを追加

MainPage.xamlにCarouselViewControlを配置して、MainViewModel.PageInfoをデータバインディングします。

UseAlexCarouselViewApp\Views\MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:cv="clr-namespace:CarouselView.FormsPlugin.Abstractions;assembly=CarouselView.FormsPlugin.Abstractions"
       x:Class="UseAlexCarouselViewApp.Views.MainPage"
       Title="{Binding Title}">

  <StackLayout>
    <Label Text="Hello" />
    <Editor BackgroundColor="Yellow" />

    <cv:CarouselViewControl
      VerticalOptions="FillAndExpand"
      HorizontalOptions="FillAndExpand"
      Position="0"
      ShowIndicators="True"
      ShowArrows="True"
      ItemsSource="{Binding CarouselPageInfo}">
      <cv:CarouselViewControl.ItemTemplate>
        <DataTemplate>
          <StackLayout VerticalOptions="FillAndExpand"
                 HorizontalOptions="FillAndExpand"
                 BackgroundColor="{Binding BackColor}">
            <Label Text="{Binding Name}" TextColor="{Binding ForeColor}"/>
          </StackLayout>
        </DataTemplate>
      </cv:CarouselViewControl.ItemTemplate>
    </cv:CarouselViewControl>
  </StackLayout>

</ContentPage>

ポイントは以下の通り。

  • コントロールを使うのでページにnamespaceを追加する
    以下により cvタグプレフィックス で CarouselViewControl が使えるようになります。
xmlns:cv="clr-namespace:CarouselView.FormsPlugin.Abstractions;assembly=CarouselView.FormsPlugin.Abstractions"
  • CarouselViewControlを配置
    親要素(ContentPage)で名前空間定義したタグプレフィックcvを使ってコントロールを配置定義しています。
    ItemsSourceとかDataTemplate定義は普通のデータバインド対応コントロールと同じテーストです。
<cv:CarouselViewControl
...
  • CarouselViewControlはView派生クラス
    CarouselViewControlはView派生クラスなのでStackLayoutの子要素としてLabelやEditorと並べて配置可能です。

3.6. ViewRendererの初期化を追加

最後に重要な「ViewRendererの初期化」処理とiOS/androidそれぞれのプロジェクトに追加します。

androidは、UseAlexCarouselViewApp.Android\MainActivity.csに「CarouselViewRenderer.Init();」を追記します。

// UseAlexCarouselViewApp.Android\UseAlexCarouselViewApp.Android\MainActivity.cs
...
using CarouselView.FormsPlugin.Android;  // ←これも追記
...
namespace UseAlexCarouselViewApp.Droid
{
  ...
  public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
  {
    ...
    global::Xamarin.Forms.Forms.Init(this, bundle);
    CarouselViewRenderer.Init();  // ←追記
    ...
  }
}

iOSは、UseAlexCarouselViewApp.iOS\AppDelegate.csに「CarouselViewRenderer.Init();」を追記します。

// UseAlexCarouselViewApp.OS\UseAlexCarouselViewApp.iOS\AppDelegate.cs
...
using CarouselView.FormsPlugin.iOS;  // ←これも追記
...
namespace UseAlexCarouselViewApp.iOS
{
  ...
  public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
  {
    ...
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
      global::Xamarin.Forms.Forms.Init();
      CarouselViewRenderer.Init();  // ←追記
      ..
    }
  }
}

※ViewRenderer
ViewRendererは「Formsとネイティブコントロール要素との繋ぎ役」みたいな役割を持っています。
Forms上でのXXコントロールandroidでは○○コントロールとして表示し、iOSでは△△コントロールとして表示する。また、各ネイティブで発生したアクション・イベントをFormsコントロールのイベントに結び付けるような、そんな役割を持っています。

4. ソースはココ

サンプル実装は↓↓↓↓↓に置いときました。

github.com

db tech showcase 2017(1日目)に参加した話

今回の投稿は、いつもの、技術を論じるブログではなく読み物です^^;

2017/9/5、db tech showcase 2017に参加しました。

私はDatabaseを軸として打ち出しているイベント(セミナー)に参加したのは実は初めてだったりします(「私はDB屋では無い」のです)。
秋葉原UDXでがっつり3日間開催されました(されています)が、これ、参加費用無料!なんですよね^^
ミニ展示スペースがあった(&プロダクト宣伝的セッションもあった)ので、スポンサー費用(+インサイトテクノロジー様の持ち出し?)なんかで運用されてるのかな?・・・なんて金勘定の頭が働いてしまうは私が穢れたエンジニアだからでしょうか^^;

初日の本日は2コマ目から参加させていただきました。

では、以下に感想をポロポロと・・・

1. 「A12:Keynote Speech これからの'‘本命技術’‘はこう見つける!~ポスト・リレーショナルデータベース時代を読み解くコツ~」

まずは、キーノート2として、ウルシステムズ株式会社 漆原 様 / 楽天株式会社 森 様によるトークセッションでした。
特定の技術に深く踏み込むものではなく、DBを中心として業界全体を俯瞰した形でのトーク、また、楽天技術研究所というテクノロジーの集まる現場における、技術との付き合い方(まとめすぎかな?)的な内容のトークが展開されました。

個人的に最も感じたのは、「お二方の下で働くエンジニアは幸せだろうな」ということです。
ビジネスを推進する要素は「ビジネス的視点を起点とするもの」「技術的視点を起点とするもの」の2つがあると私は信じています。
前者が絶対であり、技術を語るものは「ビジネスを理解しない ただのオタ」と判断される組織も日本には多々あるように感じています。でも、それが真であるケースも多々あるか・・・。

でも、やはり、最終的にテクノロジーによりサービスは顧客に対して提供されており、同時にビジネス的観点も重要であると思っています。
ビジネスマンとエンジニア、互いが、互いを否定しあうのではなく、理解しあって、「顧客に対する価値を提供できた」時が最も大きな力を発揮するような気がしているのです。

2. 「A13:MySQL MySQLを割と一人で300台管理する技術」

すみません、私はDB界隈の人間ではないので「@yoku0825さん」を存じ上げていなかったのですが、セッションは何気に楽しめました。
完全にDBエンジニアセッションでした。私はMySQLの面倒を見る仕事をしたことがなかったので、心に沁みる「おーー」という気持ちにはならなかったのですが、実運用する上で、ここ気を付けようよ、というノウハウが詰まっていて、「是非このパワポスライド欲しいわー」って感じのセッションでした。
あとGMOグループってよく耳にする企業なので、「へー、こんな感じなんだなぁ。MySQLを軸にしてるんだぁ」といった感覚を覚えました(全グループ企業の状況なんかは、もっと詳細を聞かないと分からないと思いますが・・・)

3. 「A17 データベース・ビフォーアフター ~インメモリーによる超高速化の世界~」

再び ウルシステムズ株式会社 様のセッションです。
漆原社長は、プレゼン上手&惹き付け上手 ですね^^
いえ、まあそれを除外しても、このセッションは面白かった気がしました。
apache geode」を使ったインメモリグリッドデータベースによるパフォーマンス改善事例の紹介、でした。

冒頭の「私はDB屋では無い」宣言の通り、従来のRDB文化をぶち壊すアーキテクチャが私の好物です^^
なので「インメモリグリッドデータベース」で、旧来のRDBシステムのパフォーマンスボトルネックを非RDBでぶっちぎる事例はすごくワクワクしました。
セッションのお話を聞いた限り、apache geodeは結構自動的に水平スケーリング&分散(って言っていいのかな)してくれるそうなので、これはちょっと自分でもリサーチしてみたい気になりました。

4. まとめ

上記以外のセッションも参加させていただいたのですが、特に印象的なセッションのみ感想を書かせていただきました。
あー、でも「E15 分散グラフデータベース - DataStax Enterprise Graph 森下 雄貴 様」も自分好みでしたよ。
でも、グラフデータベースはAzure Cosmos DBを学ぶと同時に学習していたので、心に刺さるレベルとまではいかなかったかも・・
まあ、私は「RDB駆逐してやるぜ!」という意気込みの「アプリケーションエンジニア」として参加させていただきましたが、十分に楽しめた気がしています。
(過激表現、ご勘弁を・・・)
まあ、なんだかんだ言いながら・・・
アプリケーションエンジニアもデータベースエンジニアも、進化や改善を求め歩みを止め無いことが最も素晴らしいことではないかと思っています!!

Dictionary to Objectのマッパーを使う - Slapper.AutoMapper

本日、会社でDictionaryオブジェクトからObjectへのオートマッパーを行う方法知りませんかー?と聞かれまして・・・
決め打ちの固定プロパティへのマッピングであれば、AutoMapperのMapFromで出来るかなぁ・・・と思ったのですが・・・
要件としてはプロパティ名を決め打ちしない動的マッピングなのです。

ということでググったら、StackOverflow経由で Slapper.Automapper なるものを見つけましたので、ここにメモしておきます。
といっても結構前からgithubに上げられていたライブラリになります。

Slapper.AutoMapper

これです!

github.com

普通にVSからNugetで取得することができます。

Slapper!!!なんかかっこいいですね。そのままの動きをします。

何をするもの?

まあ、つまり「Dictionaryオブジェクトから任意のオブジェクトへのマッパー」を可能にします。

以下の リスト1 のオブジェクトを、一発で リスト2 のオブジェクトにマッピングします。

// リスト1
var src = new Dictionary<string, object>();
src.Add("IntProp1", 10);
src.Add("IntProp2", 20);
src.Add("IntProp3", 30);
src.Add("IntProp4", 40);
src.Add("IntProp5", 50);
src.Add("StringProp1", "10");
src.Add("StringProp2", "20");
src.Add("StringProp3", "30");
src.Add("StringProp4", "40");
src.Add("StringProp5", "50");
// リスト2
public class TestClass
{
  public int IntProp1 { get; set; }
  public int IntProp2 { get; set; }
  public int IntProp3 { get; set; }
  public int IntProp4 { get; set; }
  public int IntProp5 { get; set; }

  public string StringProp1 { get; set; }
  public string StringProp2 { get; set; }
  public string StringProp3 { get; set; }
  public string StringProp4 { get; set; }
  public string StringProp5 { get; set; }
}

使い方

以下のリスト3が使い方のサンプルです。

// リスト3
namespace ConsoleApp1 {
  class Program {
    static void Main(string[] args) {
      // データソースを作成
      Dictionary<string, object> src = new Dictionary<string, object>();
      src.Add("IntProp1", 10);
      src.Add("IntProp2", 20);
      src.Add("IntProp3", 30);
      src.Add("IntProp4", 40);
      src.Add("IntProp5", 50);
      src.Add("IntProp6", 60);  // マップ先に存在しないプロパティは無視される
      src.Add("StringProp1", "10");
      src.Add("StringProp2", "20");
      src.Add("StringProp3", "30");
      src.Add("StringProp4", "40");
      src.Add("StringProp5", "50");
      
      
      // 一発でマッピング!!
      var desc = Slapper.AutoMapper.Map<TestClass>(src);
    }
  }
  
  // マップ先のオブジェクト
  public class TestClass
  {
      public int IntProp1 { get; set; }
      public int IntProp2 { get; set; }
      public int IntProp3 { get; set; }
      public int IntProp4 { get; set; }
      public int IntProp5 { get; set; }

      public string StringProp1 { get; set; }
      public string StringProp2 { get; set; }
      public string StringProp3 { get; set; }
      public string StringProp4 { get; set; }
      public string StringProp5 { get; set; }

      public string NothingText { get; set; } // ソースにないプロパティはnull(number系の型なら 0 )
  }
}

ちなみに AutoMapper と名が付きますが、いわゆる本家AutoMapperは全く内部的にも使っていません。
githubで公開されているので、ソースを見ればわかりますが、シンプルなReflectionによるMapper解決を行っています。

※最近ブログをさぼっていたのでウォーミングアップ的ブログ投稿でした。
でも、何気に要求的にはよくありそうな話なので、本投稿がGoogleで引っかかってどなたかのお役に立てれば^^です!

C#7.1のAsync Mainがお気に入り~Visual Studio 2017 Update 3(15.3)

Visual Studio 2017 Update3(15.3)が 2017/8/14 にリリースされました。

www.visualstudio.com

.NET Core 2.0サポートだったり、コンパイルベースのAzure Functionsサポートのような今までPreview版を利用しなければならなかった機能が正式版で使えるようになりました。

1. C# 7.1サポート

で、その中に「C# 7.1言語機能の追加」ってものもありました。

C# 7.1機能として以下のような機能が使えるようになりました。

  • async Main methods
  • pattern-matching with generics
  • “default” literals
  • inferred tuple names

2. async Main methodのサポート

個人的には「async Main methods」が何気にお気に入りです。

普段の開発作業においてサンプル実装を試すのに、コンソールアプリプロジェクトを利用することが結構あります。
この時、従来はMain()メソッドにasyncを付けることができなかったので、以下のようなことをする必要がありました。

↓↓↓こんなやり方や・・・

// リスト1

// C# 7.0までは・・・。
// Main()以外に非同期版メソッドを用意して呼び出し
static void Main(string[] args)
{
  MainAsync().GetAwaiter().GetResult();
}

private static async Task MainAsync()
{
  string html = await new HttpClient().GetStringAsync("http://www.yahoo.co.jp");
  Console.WriteLine( html );
}

↓↓↓こんなやり方など・・・

// リスト2

// C# 7.0までは・・・。
// 試したいメソッドに .Result をつける(実際の使い方と異なる形になる)
static void Main(string[] args)
{
  string html = await new HttpClient().GetStringAsync("http://www.yahoo.co.jp").Result;
  Console.WriteLine( html );
}

C# 7.1では以下のような実装が可能になりました。

// リスト3

// C# 7.1版
// Mainにasyncが付けられる!!
static async Task Main(string[] args)
{
  string html = await new HttpClient().GetStringAsync("http://www.yahoo.co.jp");
  Console.WriteLine( html );
}

Main()メソッドにasyncが付けられるようになったことで、直接的に実装を記述することができるようになりました。

3. 利用する C# のバージョン

利用する C# のバージョンはプロジェクトのプロパティで設定することができます。
デフォルトでは「最新のメジャーバージョン」が既定になっています。つまり VS2017 Update3(15.3) で作成したプロジェクトは、そのまま C# 7.1 が利用可能です。
プロジェクトで明示的に利用する C#バージョン を設定する方法は以下の通りです。

(1) プロジェクトプロパティの表示

ソリューションエクスプローラでプロジェクトをマウス右ボタンクリック。
表示されたメニューから「プロパティ」を選択します。

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

(2) C#バージョンの選択

表示されたプロパティ ペインから「ビルド」を選択し、「詳細設定」ボタンをクリックします。

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

表示された「ビルドの詳細設定」ウィンドウで、「言語バージョン」を選択します。

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

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