はまったりひらめいたり…とか…

.Net系プログラムで勉強したこととか嵌ったことについて書いたりします。

フロントエンドのMicrosoft Graphことはじめ

はじめに

使用するライブラリは下記バージョンとなります。

また、Graph使用部分はAngularになるべく依存しない形で構成していますが

手を抜きたい箇所がちょこちょことあるのでベースは下記Ver.のAngularで作成しています。

  • @angular/cli: 8.3.0
  • Angular: 8.2.3

それぞれの内容について記事をご覧になるタイミングによっては、画面、仕様が変更されている可能性があるのでご留意くださいませ。

なにがしたいか

フロントエンドだけでMicrosoft Graphから情報取得したい。

以上。

実装

1.Azure AD Applicationの準備

例のごとく、Graphへのアクセストークン取得用のAzure AD Applicationを作成します。

今回はOutlookで管理しているメールの情報などを取得するため

Microsoft個人アカウントにアクセスできるアプリケーションを作成します。

ADアプリを作成する際に「個人アカウント」が使用できるタイプを選択します。

f:id:TakasDev:20190910223352p:plain

2.ライブラリのインストール

Httpの通信ベースでもGraphからは情報は取得できますが

.net同様公式からClientライブラリが提供されていますので、それを使用します。

インストールするライブラリは下記のとおりです。

msal.js

今回、個人アカウントの情報にアクセスするためAzureAdv2アプリケーションを使用します。

そのためADALではなくMSALを使用します。

Graphにアクセスする際に使用するアクセストークンの取得に使用します。

npm install msal --save

@microsoft/microsoft-graph-client

Microsoft Graphとやり取りすためのライブラリです。

npm install @microsoft/microsoft-graph-client --save

@microsoft/microsoft-graph-types

Clientには型情報がついてこないようです。

TypeScriptを使用している方は@microsoft/microsoft-graph-types もインストールしておくと

Graphから取得したデータのマッピングが幾分か楽になると思います。

後述するクエリオプションの$selectを使用するのであればいらないかなとは思います。

npm install @microsoft/microsoft-graph-types --save-dev

3. msal.jsのイニシャライズ

Graphからデータを取得するためには、何はともあれアクセストークンを取得する必要があります。

リダイレクト時に読み込まれるページ、あるいはリダイレクト時に読み込まれるJS等でmsalのイニシャライズを行います。

JSでClassを使用している場合はconstructorでイニシャライズを行います。

とにかくリダイレクト時にUserAgentApplicationが生成されるようにしておきます。

import * as msal from 'msal';

export class AuthService {
    private authClient: msal.UserAgentApplication;
    constructor() {
        this.authClient = new msal.UserAgentApplication({
            auth: {
                clientId: '<ADアプリのClientID>',
                authority: 'https://login.microsoftonline.com/common'
            }
       });
    }
}

UserAgentApplicationのコンストラクタの引数で、使用するAzureADアプリケーションの情報を与えます。

今回Microsoft個人アカウントの情報を取り扱いたいので、authorityhttps://login.microsoftonline.com/commonを指定します。

テナントに紐づく情報のみに抑えたい場合はhttps://login.microsoftonline.com/<tenant-id>でOKです。

4. GraphClientのイニシャライズ

先に記述したとおり、Graphからデータを取得するためには

AzureADアプリケーションから提供されるアクセストークンが必要となります。

GraphClientはイニシャライズ時に、Graphアクセス用のトークンを取得するauthProviderを設定することができます。

Providerを指定せずにGraphアクセス時にヘッダにアクセストークンを設定する事も可能ですが

アクセス都度ヘッダを設定する処理を書くのも邪魔くさいと思うのでProviderを設定するほうがいいかなと思います。

import * as graph from '@microsoft/microsoft-graph-client';

export class GraphService {
    private client: graph.Client;
    
    constructor() {
        this.client = graph.Client.init({
            authProvider: async (done) => {
                const token = await this.authService.acquireToken({ scopes: [ 'mail.read' ] });
                if (token) {
                    done(null, token.accessToken);
                } else {
                    done('Can not Get Token', null);
                }
            }
        });
    }
}

authProviderとして、通信を行う際のトークン取得処理を記述します。

3.で設定したmsal.jsのファンクションacquireTokenを使用しアクセストークンを取得します。

acquireTokenの引数で指定するスコープ情報は、使用したいリソースの権限情報をドキュメントで参照してください。

例えば、Messageの権限情報はこちらから参照できます。

acquireTokenはPromiseで値を返却してくれるので、async-awaitでアクセストークンを取得し

authProviderのCallbackdoneの引数にトークンを指定し実行します。

done(error: any, accessToken: string | null)を引数で実行されるCallbackです。

トークンが取得できなかったりエラーが発生した場合はaccessTokenをnullにし、errorにエラー情報を格納し

逆にトークンが取得できた場合はaccessTokenに取得したトークンを設定し、errorにnullを格納し実行します。

5. Graphの実行

下準備は整いました。GraphClientを使用し、自分に送信されたメールの情報を取得してみようと思います。

export class GraphService {
    private client: graph.Client;
    ....
    getMessage() {
        this.client.api('/me/messages').get()
    }
}

処理を組み立てるときはMicrosoftのドキュメントにお世話になります。

例えばメッセージを一覧表示する - Microsoft Graph v1.0 | Microsoft Docs

.api()内のURLの指定は、この「HTTP要求」にあるアドレスを指定すればOKです。

と、いいますか、ページ下部にあるほぼそのまんまですね。

6. 複数ページにまたがるデータの取得

メールや予定表など、データ数がべらぼうに多そうなデータは一回のリクエストで全件データ取得できません。

あるいは自分で1回あたりの取得データの件数を絞ったりもできます。

アプリで Microsoft Graph データをページングする - Microsoft Graph | Microsoft Docs

その場合、@odata.nextLinkというプロパティに次ページのリンクURLが格納された状態で、Graphからデータが帰ってきます。

なので、全件データを取得したい場合は@odata.nextLinkに、次ページへのリンクが格納されなくなるまでループを回し続けなければならないというわけですね。

private messageList: Message[] = []; /* Message:@microsoft/microsoft-graph-types で提供されている型情報 */

// 初回は「/me/messages」で以降はnextlinkのURLが引数に
getMessage(url: string) {
    this.client.api(url).get().then(x => {
        (x.value as Message[]).forEach(element => {
            this.messageList.push(element);
        });
        if (x['@odata.nextLink'] !== '') {
            const nestNextLink = x['@odata.nextLink'];
            this.getMessage(nestNextLink);
        }
    });
}

7. クエリオプション

特定条件でのデータの絞り込み。取得データを特定の項目のみにする絞り込み等が行なえます。

何が行えるかは下記のドキュメントを参照してください。

クエリ パラメーターを使用して応答をカスタマイズする - Microsoft Graph | Microsoft Docs

その中からいくつかのクエリオプションを見てみます。

$select

取得する項目を指定する事ができます。

通信でどのような項目のやり取りされるかはAPIドキュメントを参照すればOKです。

今回使用してみた/messageのプロパティは下記のドキュメントで確認できます。

メッセージ リソースの種類 - Microsoft Graph v1.0 | Microsoft Docs

このプロパティから取得したい項目を選択することになります。

GraphClientでは.selectがそれにあたります。

下記のように.selectで取得したい項目名を指定します。

export class GraphService {
    private client: graph.Client;
    ....
    getMessage() {
        this.client.api('/me/messages')
            .select('subject, receivedDateTime')
            .get()
    }
}

messageのメール表題のsubjectと受信日時のreceivedDateTimeを取得しています。

$filter

取得する項目の絞り込みを行うことができます。

$select同様、絞り込む項目はAPIドキュメントを参照します。

GraphClientでは.filterを下記のように使用します。

filterの論理演算子については下記のドキュメントを参照してください。

クエリ パラメーターを使用して応答をカスタマイズする - Microsoft Graph | Microsoft Docs

export class GraphService {
    private client: graph.Client;
    ....
    getMessage() {
        this.client.api('/me/messages')
            .filter('isRead+eq+false')
            .get()
    }
}

messageの内未読のもののみを取得しています。

まとめ

フロントエンドのみでGraphのデータにアクセスする方法を確認しました。

  • Graphへのアクセスは@microsoft/microsoft-graph-clientを便利に使用しましょう
  • Graphの使用の仕方は公式ドキュメントが一番参考になります
  • クエリオプションを使用してGraphを使いこなしましょう

今回検証に使用したサンプルは下記のリポジトリとなります。

github.com

MSAL.js+Azure AD v2を触ってみる

はじめに

使用ライブラリは下記のバージョンでお送ります。

msal.js: 1.0.1

SPAっぽいなにかを作りたかったのでAngularも使用しています。

@angular/cli: 8.0.0

ただ、記述するソースコードの殆どはmsal.jsのヴァニラなものを使用しているので

他のフレームワークやバニラJSでも問題なく動作すると思います。

Azureは2019/6時点の画面をキャプチャしています。

それぞれの内容について記事をご覧になるタイミングによっては、画面、仕様が変更されている可能性があるのでご留意くださいませ。

あらまし

マイクロソフトのアカウント認証に便利なJavascriptのライブラリ、msal.js

github.com

長らく0.x.xとβ?みたいな扱いだったのですが、いつの間にか(5/4)に1.0.0とメジャーバージョンアップしていたので改めて使い方を復習してみようと思います。

この記事ではmsal.jsの使い方にのみフォーカスを当てます。

Azure AD アプリケーションの作り方とかADのスコープの設定とか

その周りの話については割愛していきます。

Azure AD v2については流れ上触る機会があったので簡単に。。。

単純にサインイン

msal.jsのREADMEにある通り、まずはClientIdのみを指定してログイン処理を実行してみます。

import * as msal from 'msal';

export class SampleAuth {
  private msalClient: msal.UserAgentApplication;

  login() {
    const con: msal.Configuration = {
      auth: { clientId: 'd476053b-7f9a-4c9a-9241-cbd54714266b' }
    };
    this.msalClient = new msal.UserAgentApplication(con);
    this.msalClient.loginPopup({scopes: ['openid']}).then(
      res => { console.log(res); }
    );
  }
}

単純に書くとこんなところですね。

何はともあれ実行してみましょう。

f:id:TakasDev:20190614222439p:plain

いつものログイン画面が表示されました。

ログイン後、バッチリアカウント情報も取得できているようです。

f:id:TakasDev:20190614222623p:plain

注意点-LoginPopup

LoginPopupを行う場合、ポップアップされた画面で認証画面からのリダイレクトが発生します。

しかし、リダイレクト先でUserAgentApplicationインスタンスが生成されていないと

ポップアップ画面が閉じられず残りっぱなしになってしまうようです。

loginPopup doesn't properly close itself · Issue #174 · AzureAD/microsoft-authentication-library-for-js · GitHub

ログイン用のボタン押下でインスタンス生成する処理になっている場合はこの部分で嵌りそうです(ハマった)。

リダイレクトページのコンストラクタなどでインスタンスを生成するようにしておく必要があります。

トークンを取得する

何はともあれトークンを取得しないことには始まりません。

ログインができたのであれば、トークンを取得してみましょう。

acquireTokenSilentを使用し、openid のスコープでトークンを取得してみます。

login() {
    const con: msal.Configuration = {
        auth: { clientId: 'd476053b-7f9a-4c9a-9241-cbd54714266b' }
    };
        this.msalClient = new msal.UserAgentApplication(con);
        this.msalClient.loginPopup({scopes: ['openid']}).then(
        res => {
            console.log(res);
            this.msalClient.acquireTokenSilent({ scopes: ['openid' ] }).then(token => {
                console.log('get token');
                console.log(token);
            });
        }
    );
}

f:id:TakasDev:20190614224640p:plain

トークンが取得できました。

The provided value for the input parameter 'response_type' is not allowed for this client....

上記のようなエラーが出る場合は認証に使用しているADアプリケーションの暗黙的フローの許可がされていない場合があるので確認してみてください。

生成されたトークンがどのようなものか

JSON Web Tokens - jwt.io で確認してみます。

f:id:TakasDev:20190614225026p:plain

issでADのテナントID、audで使用したADアプリケーションIDが設定されていることが確認できます。

取得したトークンで、ASP.net Coreで構築した認証付きのWebAPIにアクセスしてみます。

f:id:TakasDev:20190615194201p:plain

しかし、APIにアクセスはできませんでした。

それは当然で、生成されたJWTのaudは認証用に使用しているAzure ADアプリケーションのClientIdで

APIの認証で使用されているAzure ADアプリケーションのClientIdとは異なるからです。

WebAPIのClientIdは "ClientId": "792dd3ef-a306-4288-b12d-5aff71f16193" が指定されています。

WebAPI側のClientIdを変更してもいいのですが、それも芸がないので、別の方法を使用してみます。

AzureADアプリケーションの設定変更

認証で使用しているAzureADアプリケーションで、作成したWebAPIのPermissionを通しておきます。

f:id:TakasDev:20190615195221p:plain

こんな状態ですね。

スコープの変更

追加されたAPI認証にしているアプリケーションのスコープは下図で取得できます。

f:id:TakasDev:20190616162533p:plain

早速、スコープを指定し、トークンを取得してみます。

this.msalClient.acquireTokenSilent({ scopes: [ 'user.read' ] })

となっていたものを

this.msalClient.acquireTokenSilent({ scopes: [ '<APIのスコープ>' ] })

のように変更してみます。

これでトークンが取得できました。

注意点-Authorityの指定

Authorityを未指定のままでトークン取得処理を実行すると下記のようなエラーが発生しました。

AADSTS501941: Resource '792dd3ef-a306-4288-b12d-5aff71f16193'(WebApplication1) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant.

標準で使用されている認証のURL https://login.microsoft.com/common は使用できず、テナントの指定を促されました。

その場合は、msal.jsのインスタンス生成時にauthorityを指定することで回避できます。

    const con: msal.Configuration = {
      auth: {
        clientId: 'd476053b-7f9a-4c9a-9241-cbd54714266b',
        authority: 'https://login.microsoftonline.com/<テナントのGUID>'
      }
    };
    this.msalClient = new msal.UserAgentApplication(con);

注意点 - トークンの指定

ADのトークンは仕様として、複数のリソースのトークンを一挙に取得できないようです。

複数のリソースの承認を取得する (Microsoft Authentication Library for .NET) | Microsoft Docs

なので、スコープ指定時に、 scopes: [ 'user.read', '<APIのスコープURI>' ] のようにリソースをまたがる設定にすると

AADSTS28000: Provided value for the input parameter scope is not valid because it contains more than one resource.

上記のようなエラーとなります。トークンの取得は単一リソースごとに取得が基本ということですね。

Azure AD アプリケーションのエンドポイントを v2の設定に

さて、取得できたトークンを解析してみると、aud は下図のような状態でした。

f:id:TakasDev:20190616211920p:plain

ほしいのはClientIdであって、スコープではありません。

Azure AD v2であればClientIDを取得できそうなので(情報元は失念。。。)Azure AD アプリケーションの設定を変更します。

f:id:TakasDev:20190616171548p:plain

accessTokenAcceptedVersionnull2 に変更しました。

これで再度トークンを取得してみます。

f:id:TakasDev:20190616212403p:plain

お望みのトークンを取得できました。

ASP.net Core WebAPIの実装

さて、トークンは帰ってきましたが、Azure AD v2を使用することでissのエンドポイントが変わってしまいました。

(URLの末尾に/v2.0がついた)

GitHubにサンプルが転がっていたのでそれと同じように実装してみましょう。

GitHub - Azure-Samples/active-directory-dotnet-native-aspnetcore-v2: Calling a ASP.NET Core Web API from a WPF application using Azure AD v2.0

と、いってもやっていることは非常に単純で 上記GitHubで管理されているMicrosoft.Identity.WebAPIプロジェクトで参照して

startup.cs の認証部分を少し書き換えるだけですね。appsettings.jsonも書き換え必要ありません。

(Microsoft.Identity.Web はNuGetで公開されていないのですね…)

f:id:TakasDev:20190616214438p:plain

startup.cs

- services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
-   .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
+ services.AddProtectWebApiWithMicrosoftIdentityPlatformV2(Configuration);

msal.jsで取得したトークンで、APIを使用できるようになりました!

f:id:TakasDev:20190616222634p:plain

まとめ

msal.jsの基本的な使用方法と派生してAzure AD v2の認証方法を抑えることができました。

ただ、Azure ADv2は各所みるにまだまだ開発中の匂いがプンプンしますし

AzureADアプリケーションを使用して認証を行う場合は

v1エンドポイントを使用して、adal.jsを使用するほうがまだまだ無難なようです。

力尽きたので、Azure AD B2Cとmsal.jsの認証は別の機会に記事にしようと思います。。。

Azure Front Doorを使ってみる

はじめに

2019/6/2時点のAzureを使用しています。

記事をご覧頂いているタイミングによっては

記事内に出てくる画像、設定内容などが変更されている可能性があることをご留意くださいませ。

あらまし

先日のde:code19でデプロイ王子のアンプラグドイベントに参加しました。

API Managementが死んだ際の対応についてお話を聞いたところ

AzureFrontDoorもイイよ。と教えていただきました。

が、触ったことないので触ってみよう!というのが今回のあらましです。

この記事では、基本的な設定内容と使い方、キャッシュ機能の検証について記載しています。

なにはともあれ価格帯

個人で触るか会社で触るかの分水嶺ですね。Azure死するのも嫌ですし。

価格 - Front Door | Microsoft Azure

f:id:TakasDev:20190602100315p:plain

油断してたら結構持ってかれそうですが、時間単位で見ればまぁ普通にお安め。

作ってすぐ潰す検証環境であれば1,000円以内で済みそうです。

作ってみる

Azure クイック スタート - アプリケーションの高可用性を実現するフロント ドア プロファイルを Azure portal を使って作成する | Microsoft Docs

新しいサービスを使うときは基本ドキュメントに則るのが一番かなと思うので

上記のドキュメント通りに設定していきます。

結果非常に簡単に作成&使用できました。

FrontDoor作成

Configurationタブで下図のような設定画面が表示されるのでそれぞれ「+」で設定していきます。

f:id:TakasDev:20190602101149p:plain

FrontEndHost設定

f:id:TakasDev:20190602101402p:plain

SESSION AFFINITYはバックエンドなんかでCookieを使用した処理や認証を使用している際に使うものでしょうか。

ひとまず現状はセッション情報は使用しないので Disabled に設定しておきます。

BackEndPool設定

Backendの登録は悩まずに設定することができました。

f:id:TakasDev:20190602102748p:plain

HEALTH PROBES

Azure Front Door Service - バックエンドの正常性監視 | Microsoft Docs

上記ドキュメントによると正常性の特定は200コードで判定されるようです。

なので、バックエンドで認証を設けている場合等は疎通試験用の無認証のAPIを用意したほうが良さそうです。

今回自分が作ったWebAPIは認証機構を設けておりませんが、上図のPathの/は404になるので

使用できるAPIエンドポイントを指定するのが良さそうです。

今回はテストで使用する /api/values を指定しました。

ヘルスチェックを行うインターバルも指定できるようです。

頻繁にアクセスされるようなリソースはインターバルを短めに設定しておいたほうが良さそうです。

後述しますが、停止~ヘルスチェックのインターバル間にアクセスした場合、停止したエンドポイントにアクセスすることになりそうです。

LOAD BALANCING

Azure Front Door Service のバックエンドとバックエンド プール | Microsoft Docs

負荷分散の設定については今回特に留意することはなさそうです。

Ruleの設定

f:id:TakasDev:20190602104125p:plain

PATTERNS TO MATCH

パターンについては今回特に指定していないですが 特定のAPIについてのみのアクセス。とかBackendPool/Ruleが複数設定される場合に重宝しそうです。

RouteType

後の検証の関係でRouteTypeはForwardを指定しています。

キャッシュがある場合はキャッシュを使用する。という設定のようですね。Redirect はキャッシュ関係なしにAPIに直接アクセスする設定のようです。

頻繁に変更があるAPI/めったに変更がないAPIという感じにRule分離させているといいのかもしれません。

Forwardung Protocol/URL Rewriteはがっつり使う段階からになりそうなので割愛

Cachingは後の検証で使用しますが、ひとまず今はDisabledにして終わらせます。

できあがり

大凡1分くらいでリソースが作成されました。

f:id:TakasDev:20190602105609p:plain

検証

みんなだいすきPostmanを使って検証してみます。

WebAPIは下記のようにASP.net Coreで雑に文字列が返却されるものをデプロイしています。

[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "This is", "API One" }; // バックエンド1/2で返却される内容は変えている
}

まずは普通に疎通試験してみます。

f:id:TakasDev:20190602110640p:plain

問題なく使用できてそうです。

優先的にアクセスされるAPIエンドポイントを停止させました。

f:id:TakasDev:20190602110758p:plain

これで接続されるエンドポイントが変更されるはずです。

f:id:TakasDev:20190602110947p:plain

設定通り同一のエンドポイントURLで別リソースを参照していることが確認できました!

注意!!

先で設定した通りエンドポイントのヘルスチェックは30秒のインターバルで行われます。

なので、停止後即アクセスすると403が返却されます(されました。焦った…)。

停止直後に403等が返却された場合は時間をおいて再度アクセスしてみると良いかもしれません。

キャッシュ機能の検証

de:code19で王子とお話した際に「キャッシュ機能もあるよ」とお聞きしたので、キャッシュ機能も試してみます。

まずはキャッシュデータが参照される」というのがデータ取得時の動きなのだと思われます。

以下設定と検証。

まずは、FrontDoorの設定を変更します。

Cachingを有効に

f:id:TakasDev:20190602112504p:plain

Query string caching behavior

「クエリ文字列を無視」「一意なURLをすべてキャッシュ」の2種があります。

クエリ文字列を使用するURLが指定された場合のキャッシュ動作のようですね。

クエリ文字列を無視してキャッシュするか、クエリ文字列含めたURLでキャッシュするか。といった設定のようです。

ここはひとまずCatche every Unique URLでクエリ文字列含めたURLでキャッシュするようにしておきます。

検証

クエリ文字列の検証も含めるたいので、Controllerの内容もちょっと変更しました。

[HttpGet(Name = "Get Single Data")]
public ActionResult<string> GetSingleData([FromQuery]int id)
{
    return $"This is API One And QuerId={id}!";
}

クエリ文字列で指定された文字列含め返却するよう変更しています。

まずは普通に実行した結果です。

f:id:TakasDev:20190602125843p:plain

queryで指定された文字列が返却されていることが確認できます。

次にバックエンドのAPIに使用しているWebAppsを停止したうえで実行してみます。

f:id:TakasDev:20190602130109p:plain

もう一方のバックエンドは参照せずに、キャッシュされたデータが返却されているようです。

query文字列を変更した状態で実行してみます。

f:id:TakasDev:20190602115310p:plain

こちらはデータがキャッシュされていないので停止されていないAPIの方を参照していることが確認できます。

Ignore Query Stringsの設定にした場合

queryでid=300を指定して、実行してみました。結果は下図のとおりです。

f:id:TakasDev:20190602122038p:plain

では、idを変更して実行してみます。

f:id:TakasDev:20190602122154p:plain

idを変更しても返却される値は変更されず、queryの文字列を無視してキャッシュを行っていることが確認できました。

おわりに

Azure Front Doorを触ってみましたが、簡単に設定/使用することができることがわかりました。

キャッシュ機能については「はじめにキャッシュ」という動き方であるが故に

頻繁に更新が発生するデータの場合などには、すこし気をつけて使う必要がありそうです。

キャッシュが有効な期間とか、リソース使用の優先順位とか、キャッシュについて色々設定できると嬉しいかもですね。

ReactiveFormsで使用できるカスタムコンポーネントを作成する

はじめに

本記事ではAngularは下記Versionを使用しています。

  • AngularCLI: 7.3.6
  • Angular: 7.2.10

ゴール

Inputコントロールなんかと同様に formControlName を指定できるような

ReactiveFormsで使用できるComponentを作成します。

今回は「ダイアログ表示ボタン+入力ダイアログ」を一つのコンポーネントとし

そのコンポーネントをReactiveFormsで使用します。

操作イメージは下のGifのような動作イメージです。

「Result」内にあるように、ダイアログの入力内容が反映されます。

f:id:TakasDev:20190324184459g:plain

ControlValueAccessor

Angular

Angularの公式リファレンスによると、ControlValueAccessorを使用して、Valueとコントロールのリンクを行っていると記載されています。

Angular

ControlValueAccessorは、下記4つの機能のインタフェースを持つようです。

  • writeValue(obj: any) →ModelからViewへの値の書き込み
  • registerOnChange(fn: any) →View変更時のCallback
  • registerOnTouched(fn: any) → ViewTouch時のCallback
  • setDisabledState(isDisabled: boolean) → disabled/enabledをViewに反映する

全容は下記のソースです。

@Component({
  selector: 'app-address-input-dialog',
  templateUrl: './address-input-dialog.component.html',
  styleUrls: ['./address-input-dialog.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AddressInputDialogComponent),
      multi: true
    }
  ]
})
export class AddressInputDialogComponent implements ControlValueAccessor {

  private addressData: AddressModel;
  private isDisabled = false;
  private onChange: any = () => {};
  private onTouched: any =  () => {};

  constructor(
    private dialog: MatDialog
  ) { }

  openDiaog() {
    const dialog =  this.dialog.open(DialogComponent, {
      data: this.addressData
    });
    dialog.afterClosed().subscribe(data => {
      this.addressData = data;
      this.onChange(data);
      this.onTouched();
    });
  }

  // ↓ControlValueAccessorのInterface群
  writeValue(obj: any): void {
    this.addressData = obj;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

}

コントロールの動きとして、ボタン押下時にダイアログを表示

ダイアログが確定されたらデータを反映。という流れです。

そのため、dialog.afterClosed() でダイアログが閉じられた際に、formcontrolの値変更時のイベントを実行しています。

ダイアログコンポーネント側はオーソドックスなReactiveFormなので割愛します。

何が嬉しい?

下記は作成したカスタムコンポーネントを使用しているコンポーネントのソースです。

export class MyFormGroupFieldComponent implements OnInit {

  public fg: FormGroup;

  constructor(
    private fb: FormBuilder
  ) { }

  get formResult() {
    if (typeof(this.fg) !== 'undefined') {
      return JSON.stringify(this.fg.value);
    } else {
      return '';
    }
  }

  ngOnInit() {
    const initAddressData: AddressModel = { city: '', prefecture: '', zipCode: ''};
    this.fg = this.fb.group({
      userId: ['', Validators.required],
      familyName: ['', Validators.required],
      lastName: ['', Validators.required],
      address: [initAddressData]
    });
  }

  patchData() {
    const initAddressData: AddressModel = { city: '港区', prefecture: '東京都', zipCode: '000-0000'};
    const data: MyFormGroupModel = {
      userId: 'devtakas',
      familyName: 'familyname',
      lastName: 'lastname',
      address: initAddressData
    };
    this.fg.patchValue(data);
  }

}

色々と書かれているように見えますが

初期値を設定したり、固定データを反映したり、画面上に表示する文字列を作成しているだけです。

なので、「ダイアログ開いたときは値を反映して~」とか「閉じたときは変数に値を格納~」みたいな処理は記述しておりません。

@ViewChild で子コンポーネントのプロパティにアクセスする。なんてこともしていないです。

コンポーネントの処理記述が非常に簡潔になります。

またformControlNameをカスタムコンポーネントに指定することができるため

Htmlテンプレートも下記のようにシンプルに記述できるようになります。

<form [formGroup]="fg">
...
  <div>
    <app-address-input-dialog formControlName="address"></app-address-input-dialog>
  </div>
...
</form>

formに対して構造体の値そのままでやり取りできるのも良いポイントですね。楽。

参考ページ

Angular

Angularでカスタムフォーム(CustomFormControls)を作る(1/2) - Qiita

最後に

今回作成したデモは下記に格納しています。

デモソースのごった煮ですがご容赦ください ><

github.com

NLogを.Net Standard/Coreで使用する

はじめに

.Net Core等は下記バージョンでお送りします。

  • netstandard: 2.0
  • netcoreapp: 2.2
  • AspNetCore: 2.2.0
  • NLog: 4.6.3

あらまし

NLogを使うとかは今更ではあるのですが

.Net CoreのExeアプリケーションからASP.net WebAPIアプリケーションで

NLogを使用する機会があったので、学習ついでの備忘録な感じのトピックです。

NLog.configの設定を外に出す

基本的な使い方は本家のGitHubやググったら山程でてくるので割愛…

NLogのログ出力の設定は、nlog.configXML形式ファイルに記述していく感じです。

が、これはいまいち好きじゃない。

と、いうのもASPにしてもCoreの普通のアプリにしてもjsonファイルにアプリケーション設定を記述していますし

あちこちのファイルに設定内容が散らばっているのも少し邪魔くさい。

ASP.netに関してはWebAppsがもっているアプリケーション設定で出力先をいろいろ設定できるようにすれば

CI/CD側の負担も軽くなるんでは?と思ったわけです。

(nlog.configのようなXMLファイルの設定もWebAppsの設定上でできるのであればいいのですが…ない?ですよね?)

NLog.configの構成

まずはXMLで設定されるNLogの構成を見てみます。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <targets>
    <target name="console" xsi:type="Console" />
  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="console" />
  </rules>
</nlog>

これはLogの内容をConsoleに吐き出す設定ですが、target で「何に対して」ログ出力するか指定し

rule で 'target' に対して出力するログの内容を指定しています。

この場合、「Infoレベルからのログを Console に出力する」という設定になっているわけですね。

NLogのTarget/Ruleの関係は、下記サイトが参考になりました。

NLog使い方メモ - マコーの日記

NLogの設定を動的に指定する

こちらのサイトを参考にしました。

NLogをプログラマブルに初期化し動的に構成変更する - M12i.

NLog.Target 名前空間ConsoleTarget というClassが存在します。

NLog/ConsoleTarget.cs at dev · NLog/NLog · GitHub

他にも FileTargetDatabaseTarget が存在します。

Classにあるプロパティを見てみると、LayoutConnectionStringなどXMLで設定するプロパティが存在するのが確認できます。

XMLの設定を確認しながら、TargetClassの同名プロパティに値を設定していく…という方法でNLogの設定ができそうです。

で、出来上がったのが下記です。

public static void ConsoleLogInit()
{
    var conf = LogManager.Configuration;
    var console = new ConsoleTarget("console"); // consoleターゲットを生成
    console.Layout = LogLayout;  // アウトプットフォーマットレイアウトを設定
    conf.AddTarget(console);  // NLogの設定に生成したターゲット情報を追加
    conf.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, console));  // consoleターゲットを使用するルールを追加
    LogManager.Configuration = conf;  // NLogの設定に反映
}

あとは同じように設定するだけ

TargetのClassにたいしてどのようなAPIが存在してどのような引数が求められているのかは

上記のGitHubソースコードドキュメントから抑えることができるので

ファイルに出力する場合やDBに出力する場合も同じように設定していくだけです。

ファイルに出力する場合はファイル名称やエンコードを指定するAPIが追加されていたりします。

public static void WriteLogToFileInit(string filePath, LogLevel targetLogLevel)
{
    var conf = LogManager.Configuration;
    var file = new FileTarget("file");
    file.Encoding = Encoding.UTF8;
    file.FileName = filePath;
    file.Layout = LogLayout;
    conf.AddTarget(file);
    conf.LoggingRules.Add(new LoggingRule("*", targetLogLevel, file));
    LogManager.Configuration = conf;
}

Databaseはちょっと変わり種でDatabaseParameterInfoのインスタンスにLayoutを設定しないといけません。

public static void WriteLogToSqlDatabaseInit(string connectionString, LogLevel targetLogLevel)
{
    var conf = LogManager.Configuration;

    var dbtarget = new DatabaseTarget();
    dbtarget.ConnectionString = connectionString;
    dbtarget.Name = "dbtarget";
    dbtarget.DBProvider = "System.Data.SqlClient";
    dbtarget.CommandText = "Insert Into LoggingTable("
            + "Logged,"
            + ") values ("
            + "@logged,"
            + ")";
    var loggedParam = new DatabaseParameterInfo();
    loggedParam.Name = "@logged";
    loggedParam.Layout = "${date}";
    dbtarget.Parameters.Add(loggedParam);

    conf.AddTarget(dbtarget);
    conf.LoggingRules.Add(new LoggingRule("*", targetLogLevel, dbtarget));
    LogManager.Configuration = conf;
}

外部からNLogの設定を行う!

と、いうわけでソースコード上でNLogの設定が十二分に行えることがわかりました。

あとは、ASP.net WebAPIなどのappsettings.jsonなどに適当な設定を作ってあげればいいだけです。

適当に↓な感じでaapsettings.jsonを作って

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    },
    "OutputToLogFile": {
      "FilePath": "logfile.txt",
      "LogLevel": "Error"
    },
    "OutputToDatabase": {
      "ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Logging;Integrated Security=True;",
      "LogLevel": "Info"
    }
  },
  "AllowedHosts": "*"
}

Startup.cs あたりで設定ファイルを読み込んでNlogを設定すればいい感じかなと思います。

private void NLogSettings()
{
    Logging.LoggingSettings.ConsoleLogInit();
    var fileName = this.Configuration["Logging:OutputToLogFile:FilePath"];
    var logLevel = this.Configuration["Logging:OutputToLogFile:LogLevel"];
    // NLogの設定を行う~~
}

これでWebAppsのアプリケーション設定への設定のみでログ出力先の挿げ替えなどが簡単に行えるようになりました。

終わりに

今回作成したモロモロのデモは下記リポジトリになります。

ASP.net WebAPIの ActionFilterAttributeExceptionFilterAttribute 使ったり

EntityFrameworkの実行SQLをログ出力したりする実験コードも含んでたりします。

github.com

「簡単な」入力状態復帰機能をAngularのReactiveFormsで実装した話

はじめに

Angularは下記のバージョンでお送りします。

@angular/cli: 7.3.9

なにがしたかったか

  • 認証にAzure ADを使用している関係で、定期的に(1時間立ち上げっぱなしで1回程度)ページがリフレッシュされる
  • 入力内容が消えちゃうのもアレなので入力状態を保存しておいて復帰させたい。
  • 巷の入力状態復帰ライブラリはCookieやlocalstorageに保存するものがほとんど
  • じゃあライブラリ使わなくてもReactiveForms使ってたら余裕で実装できるかも?

というわけで実装してみよう…というあらましです。

行うこと

  1. ReactiveFormsのvalueChangesイベントをサブスクライブします。
  2. ReactiveFormsはデータを構造体でもっているので、そのデータをJSONにしてlocalstorageに保存します。
  3. ComponentがInitializeされた段階でlocalstorageにデータが存在する場合はpatchValueで入力されていたデータを復帰します。

以上。

極端な話、下記ソースコードだけで実現できるということですね。

(もちろん格納データの存在チェックやらは必要なのでこれ+もろもろは必要ですが!)

ngOnInit() {

    // 起動時にlocalstorageの値をFormsに反映
    const stVal = JSON.parse(localStorage.getItem('hogehoge'));
    this.sampleInputFormGroup.patchValue(stVal);

    this.sampleInputFormGroup.valueChanges.subscribe(value => {
        // 変更内容をlocalstorageに保存
        const valSt = JSON.stringify(value);
        localStorage.setItem('hogehoge', valSt)
    });

}

結果↓のGifのように動きます。

f:id:TakasDev:20190524223000g:plain

タブを閉じて再度開いても

入力状態が保持されている事がわかると思います。

まとめ

ReactiveFormsは構造体の状態で値の受け渡しができるので

ライブラリなしでも入力状態復帰処理は簡単に実装できますよ!ということがわかりました。

上の図で動いているソースのコードは下記になります。

Component - Service間をRxJSでつなげたりとか

ほんのちょっとだけ複雑になっています。

github.com

ごった煮のソースの一部で申し訳ないですが。。。 ><

KarmaやJestのカバレッジをAzureDevOpsパイプラインで収集してみた

昨日、Azure DevOps Tokyo, Japan 2nd impactに参加しました。

jazug.connpass.com

その時にAzure DevOps パイプラインでのカバレッジレポートの収集の話になり

自分が今使っているAngularのプロジェクトでも、カバレッジレポートが収集できるか試してみました。

現在、自分がAngularで行っているユニットテストは、KarmaとJestの2パターンあるので

その両方で試してみた結果となります。

はじめに

ちと古いですが、昔JestのテストとCIを組んだ構成を利用したので

Angular Cli:7.0.5で試した結果となります。

パイプラインでのカバレッジレポートのPublish

ユニットテストの結果出力同様、テストバレッジの結果出力も、専用のタスクがあるようです。

タスク名は「Publish Code Coverage Results」となっています。

パイプライン構築GUIで見てみると、下図のような設定画面となっているようです。

f:id:TakasDev:20190119232434p:plain

YAMLの設定については下記サイトに記載されています。

Publish Code Coverage Results task - Azure Pipelines | Microsoft Docs

ユニットテスト同様、結果出力されたXMLファイル指定するようです。

XMLのフォーマットとしてCobertura か JaCoCoのどちらかが指定されています。

ざっと調べてみたところ、JestもKarmaもCobertura フォーマットのXMLを吐けるようなので

Cobertura フォーマットのカバレッジレポートをパイプラインでPublishしようと思います。

Jest

Jest自身がCoberturaフォーマットのカバレッジレポートを出力する機能を持っているようなので必要最低限の変更で済みます。

AzureDevOps + Angular + Jestのテストパイプラインについては、以前の記事を参考にしてください

package.json

  "test:watch": "jest --watch",
- "test:ci": "jest --reporters=jest-junit",
+ "test:ci": "jest --reporters=jest-junit --coverage",
   "reporters": [
     "default",
     "jest-junit"
   ],
+ "coverageReporters": [
+   "text",
+   "html",
+   "cobertura"
+ ]

上記の設定で、npm run test:ciを実行すると、テスト実行時にcoverageディレクトリにカバレッジレポートが出力されます。

f:id:TakasDev:20190119234741p:plain

Karma

Karmaも簡単な変更でカバレッジレポートをcoberturaフォーマットで出力できるようになります。

angular.json

  "karmaConfig": "src/karma.conf.js",
+ "codeCoverage": true,
  "styles": [
     "./node_modules/@angular/material/prebuilt-themes/purple-green.css",
     "src/styles.scss"

karma.conf.js

  coverageIstanbulReporter: {
    dir: require('path').join(__dirname, '../coverage'),
-   reports: ['html', 'lcovonly', 'text-summary'],
+   reports: ['html', 'lcovonly', 'text-summary', 'cobertura'],
    fixWebpackSourcePaths: true
  },

karma側も同様の構成でカバレッジの結果が出力されます。

f:id:TakasDev:20190120000455p:plain

Azure DevOpsパイプライン

Azure DevOps側に戻ってきました。

出力したカバレッジレポートのXMLとレポートHTMLを取得するタスクを作成します。

KarmaもJestも、同じ名前のディレクトリに同じ名前のファイルでXML出力されているので同様の設定となります。

タスクのyamlは下記の通りです。

yaml

- task: PublishCodeCoverageResults@1
  displayName: 'Publish code coverage from $(System.DefaultWorkingDirectory)/**/cobertura-coverage.xml'
  inputs:
    codeCoverageTool: Cobertura

    summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/cobertura-coverage.xml'

    reportDirectory: '$(System.DefaultWorkingDirectory)/**/coverage'
  • codeCoverageTool : 今回Coberturaフォーマットを使用したのでCoberturaを指定します。
  • summaryFileLocation : 出力されたXMLファイルを指定します。
  • reportDirectory : カバレッジのHTMLレポートのディレクトリを指定できます。

結果

CIログのSummaryタブで全体の結果を確認できます。

f:id:TakasDev:20190120001542p:plain

CodeCoverageタブでは出力されたHTMLレポートが確認できます

f:id:TakasDev:20190120001748p:plain

デザインが統一されていないため、すこぶる見にくいです…

ダークテーマで見にくささらにドン。といった感じですね…

最後に

Angularに限らずKarmaやJestのテストフレームワークを使用している場合

コードカバレッジの結果をAzureDevOpsのCIでレポート出力するのはとても簡単だということが分かりました。

これでより良いユニットテストCIライフが送れそうですね!