athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

Angularのコンポーネント間の値受け渡しについて

こんにちは、情報システム部の高野です。
ここのところバタバタしていて全然ブログが書けませんでした。
本当に継続は難しい・・・

ということでブログが書けているってことは少し落ち着いてきたってことです(笑)
まだリリースはしていないのですが、現在携わっているシステムはフロントをAngular*1で作っています。
かなり入力項目が多いシステムなのでカテゴリごとにコンポーネント化しています。
Angularをはじめ最近のWebフロントエンドのフレームワークコンポーネント化が簡単にできるので便利ですよね!


今回の仕組みは下図のようにコンポーネントを分けています。*2
f:id:taktak1974:20180913170420p:plain
コンポーネントは、Angularのルーティングでページ遷移しますので
コンポーネントAを表示している時に存在する子コンポーネントはaとbだけです。


コンポーネント化は簡単なのですが、コンポーネント間の値の受け渡しが発生すると
気を付けて設計しないとコードがとても読みづらいものになってしまいます。
今回の仕組みで発生したコンポーネント間の受け渡しのパターンは

  1. コンポーネントAから子コンポーネントaのように「親から子」に値を受け渡しするケース
  2. コンポーネントaから子コンポーネントbのように「子から子」に値を受け渡しするケース
  3. コンポーネントaから子コンポーネントcのように「親が違う子」に値を受け渡しするケース

です。

下記で示すサンプルは全てパターン02を実装していますが、
パターン03にも応用可能です。

Serviceクラス経由で値を受け渡す

先に結論から言うとServiceクラスを経由して値を受け渡すのが簡単でシンプルです。
(公式のチュートリアルにも載ってるので一番オーソドックスなやり方なのでしょう)

コードは次のような感じになります。*3
サンプルとしては子コンポーネントaで入力し、子コンポーネントbでそれを表示するようにしています。*4

Serviceクラス
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ValueSharedService {
  // 受け渡しを行うプロパティ
  firstName = '';
  lastName = '';
}
コンポーネントa
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { ValueSharedService } from '../../value-shared.service';

@Component({
  selector: 'app-child-a',
  templateUrl: './child-a.component.html',
  styleUrls: ['./child-a.component.css']
})
export class ChildAComponent implements OnInit {

  form: FormGroup;

  constructor(private valueSharedService: ValueSharedService, private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      lastName: this.valueSharedService.lastName,
      firstName: this.valueSharedService.firstName,
    });
  }

  onChangeName() {
    this.valueSharedService.lastName = this.form.get('lastName').value;
    this.valueSharedService.firstName = this.form.get('firstName').value;
  }
}
<div [formGroup]="form">
  <div>
    <label>姓:<input type="text" formControlName="lastName" (change)="onChangeName()"></label>
  </div>
  <div>
    <label>名:<input type="text" formControlName="firstName" (change)="onChangeName()"></label>
  </div>
</div>

入力に応じてイベントが実行され、Serviceクラスのプロパティを書き換えます。

コンポーネントb(表示側)
import { Component} from '@angular/core';
import { ValueSharedService } from '../../value-shared.service';

@Component({
  selector: 'app-child-b',
  templateUrl: './child-b.component.html',
  styleUrls: ['./child-b.component.css']
})
export class ChildBComponent {

  get fullName() {
    return `${this.valueSharedService.lastName} ${this.valueSharedService.firstName}`;
  }

  constructor(private valueSharedService: ValueSharedService) { }
}
<p>
  {{fullName}}
</p>

コンポーネントbは、Serviceクラスの値を連結して表示するだけです。


Serviceクラス経由で値を受け渡す方法は、直感的でなんら引っかかるところが無いと思います。
まあServiceクラスがシングルトンだということだけ意識してれば良いかと。
非常にシンプルで分かりやすい!


さて、これを別の方法でやってみたいと思います。

@Input()と@Output()を使う

まず親から子にデータを渡す方法としては、@Input()を使います。

コンポーネントb
import { Component, Input } from '@angular/core';
import { Name } from '../../name';

@Component({
  selector: 'app-child-b',
  templateUrl: './child-b.component.html',
  styleUrls: ['./child-b.component.css']
})
export class ChildBComponent {

  @Input() name: Name;

  get fullName() {
    return `${this.name.lastName} ${this.name.firstName}`;
  }
}

HTMLは、Service経由の時と変わらないので割愛します。

コンポーネント(表示側の指定)

@Input()を使う場合は、親側からHTMLで渡したい値を指定するだけです。

<div>
  <app-child-b [name]="name"></app-child-b>
</div>

[name]の部分が子コンポーネントの@Input()を指定した変数名になります。


逆に子コンポーネントから親コンポーネントに値を渡すときは@Output()を使います。

コンポーネントa
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Name } from '../../name';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-child-a',
  templateUrl: './child-a.component.html',
  styleUrls: ['./child-a.component.css']
})
export class ChildAComponent implements OnInit {

  @Output() public nameChanged = new EventEmitter<Name>();

  form: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      lastName: '',
      firstName: '',
    });
  }

  onChangeName() {
    this.nameChanged.emit({
      lastName: this.form.get('lastName').value,
      firstName: this.form.get('firstName').value
    });
  }
}

HTMLは、Serviceクラス経由の時と変わらないので割愛します。
@Output()は、EventEmitter型になります。
onChangeName関数でこれを呼び出すことにより親コンポーネントで指定したコールバック関数が実行されます。
引数で渡したい値を指定すれば親コンポーネントに値が渡せます。

コンポーネント(入力側の指定)
<div>
  <app-child-a (nameChanged)="onChangeName($event)"></app-child-a>
</div>

HTMLで@Output()の変数に実行したい関数を渡します。
この方法は、サンプルだとシンプルであまり問題無いように見えますが
子から親に値を渡す方法がコールバック関数の引数経由になるので
処理が複雑になってくるとバグの温床になりかねません。
コンポーネントの値が変わった時に親コンポーネントでなにかしらの処理を実行したい時には有用ですが
単純に値を渡すだけのために使わない方が良いのではないかと個人的には思います。
それとパターン03に対応するのはとても複雑になります。

@ViewChild()を使う

@ViewChild()を使うと親コンポーネントに子コンポーネントインスタンスを持つことができるようになります。
コンポーネントインスタンスがあるのでpublicメンバーであればやりたい放題になります。


@ViewChildを使ったサンプルコードです。

コンポーネント
import { Component, OnInit, ViewChild } from '@angular/core';
import { Name } from '../name';
import { ChildAComponent } from './child-a/child-a.component';
import { ChildBComponent } from './child-b/child-b.component';

@Component({
  selector: 'app-parent-a',
  templateUrl: './parent-a.component.html',
  styleUrls: ['./parent-a.component.css']
})
export class ParentAComponent implements OnInit {

  @ViewChild(ChildAComponent) childA: ChildAComponent;
  @ViewChild(ChildBComponent) childB: ChildBComponent;

  constructor() { }

  ngOnInit() {
    this.childA.nameChanged = this.onChangeName;
  }

  onChangeName = (name: Name) => {
    this.childB.name = name;
  }
}

@ViewChildを使って子コンポーネントインスタンスを持つようにしています。
ngOnInit内で子コンポーネントa に対して関数を渡します。
渡した関数onChangeName 内で子コンポーネントbに対して
コンポーネントaから渡ってきたnameを渡します。

<div>
  <app-child-a></app-child-a>
</div>
<div>
  <app-child-b></app-child-b>
</div>

HTMLでは、子コンポーネントを指定するだけで値の受け渡しは無くなります。

コンポーネント側は何も工夫が無いので載せる必要も無いのですが一応

コンポーネントa
import { Component, OnInit } from '@angular/core';
import { Name } from '../../name';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-child-a',
  templateUrl: './child-a.component.html',
  styleUrls: ['./child-a.component.css']
})
export class ChildAComponent implements OnInit {

  public nameChanged: (name: Name) => void;

  form: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      lastName: '',
      firstName: '',
    });
  }

  onChangeName() {
    this.nameChanged({
      lastName: this.form.get('lastName').value,
      firstName: this.form.get('firstName').value
    });
  }
}
コンポーネントb
import { Component, OnInit } from '@angular/core';
import { Name } from '../../name';

@Component({
  selector: 'app-child-b',
  templateUrl: './child-b.component.html',
  styleUrls: ['./child-b.component.css']
})
export class ChildBComponent {

  name: Name = { lastName: '', firstName: '' };

  get fullName() {
    return `${this.name.lastName} ${this.name.firstName}`;
  }

  constructor() { }
}

このやり方では、親コンポーネント以外はAngular特有の処理が無くなるのでシンプルに見えますが
コンポーネントから子コンポーネントに対して何でもやれてしまうので
きちんとルールを設けないとカオスに陥ります。
それとこの例では問題無いのですがAngularのライフサイクルを意識して作らないと思わぬバグに嵌ることにもなります。

ReactiveForms専用の方法

この方法は今回のプロジェクトでは使用していません。
このブログを書くにあたり色々調べていたら出てきたやり方です。
これをもっと早く知っていればこの方法で実装したのになっていう方法です。

コンポーネント

import { Component, OnInit } from '@angular/core';
import { Name } from '../name';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-parent-a',
  templateUrl: './parent-a.component.html',
  styleUrls: ['./parent-a.component.css']
})
export class ParentAComponent implements OnInit {

  form: FormGroup;

  get name(): Name {
    return {
      lastName: this.form.get('name').get('lastName').value,
      firstName: this.form.get('name').get('firstName').value,
    };
  }

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({});
  }
}

ここでは空のFormGroupを作っておきます。
nameプロパティは、FormGroupから取り出した値を返すようになっています。

<div [formGroup]="form">
  <app-child-a></app-child-a>
</div>
<div>
  <app-child-b [name]="name"></app-child-b>
</div>

HTML側にはformGroupの指定を追加しています。

コンポーネントa
import { Component, OnInit } from '@angular/core';
import { FormGroupDirective, FormGroup, FormControl, ControlContainer } from '@angular/forms';

@Component({
  selector: 'app-child-a',
  templateUrl: './child-a.component.html',
  styleUrls: ['./child-a.component.css'],
  viewProviders: [
    {
      provide: ControlContainer,
      useExisting: FormGroupDirective
    }
  ]
})
export class ChildAComponent implements OnInit {

  constructor(private parent: FormGroupDirective) { }

  ngOnInit() {
    const form = this.parent.form;

    form.addControl('name', new FormGroup({
      lastName: new FormControl(''),
      firstName: new FormControl(''),
    }));
  }
}

constructorでFormGroupDirectiveを受け取ります。
このインスタンスから親のFormGroupが取得できます。

<div formGroupName="name">
  <div>
    <label>姓:<input type="text" formControlName="lastName"></label>
  </div>
  <div>
    <label>名:<input type="text" formControlName="firstName"></label>
  </div>
</div>
コンポーネントb

こちらはコンポーネントaと同様にFormGroupを貰っても良いのですが
表示するだけであれば@Input()でやるのが無難な気がします。
コードは上記@Input()を使う方法とまったく同じで動きます。


この方法であればパターン03での値の受け渡しもそこまで大変なことにはならないと思います。


ReactiveFormを使っていてパターン01・パターン02の場合は、この方法が割と良いのではないかと思いますが
実際にプロジェクトでこの方法を使っていないので複雑になってくるとどうなるかは未知数です。

まとめ

今回のプロジェクトでAngularのコンポーネント分割と値の受け渡しに関して
良いところも大変なところも大分見えました。
Angularは、コンポーネント間で値を受け渡す方法がいくつもあります。
どれが良いとは一概に言えないので都度判断して最良の方法を選択する必要があります。
なにか他のライブラリを使えばもっと簡潔に分かりやすいコードにできたかもしれません。
その辺りはまた次のプロジェクトで検討していきたいと考えてます。


弊社ではエンジニアを募集しています。
Angular以外にもVue.jsを使ったシステムもあります。
興味がある方は下記からエントリお願いします。
athome-inc.jp

*1:バージョンは5を使ってますが、このブログのサンプルコードはAngular6で書いてます

*2:本当はちょっと違うのですが、説明が大変なので簡略化しています。

*3:今回のプロジェクトではReactiveFormを使ったのでサンプルも同様にReactiveFormにしています。

*4:実際はそんな単純な感じでは無いのですが・・・

Visual StudioでC#とTypeScriptのUnitTestを一覧してみる。

ASP.NET WebForm系システムの駆逐まであと一歩。情報システム部の中嶋です。
弊社のシステム(特に社内向け)は、ASP.NET MVCASP.NET Coreで構築しているものが多数*1あります。そして、ASP.NET MVCの開発にはVisual Studioを使用*2しています。また、Web UIの開発を行っている以上、当然、.NET系の言語だけでは収まらずTypeScript*3も併用しながらの開発となります。
Visual Studio 2017は、C#などの.NET系の言語だけではなくTypeScriptでも定義や参照の検索が行え、インテリセンスも効くので非常に便利です。また、テストエクスプローラーではUnitTestの一覧が表示でき、一覧からテストを選択して実行することもできます。
が、しかし、テストエクスプローラーではTypeScriptのテストは表示されないため、TypeScriptのUnitTestだけはコマンドラインから実行することになります。
「なんか、ちょっともったいないな」と思っていたら、ありました。機能拡張が。マイクロソフトの井上章さんも紹介されている「Chutzpah Test Adapter for the Test Explorer(以下、Chutzpah)」です。
井上さんの記事は少し古くVisual Studio 2012でのお話なので、今回はVisual Studio 2017にChutzpahをインストールして使ってみたいと思います。

Chutzpahの動作概要

Chutzpahは、chutzpah.jsonで指定されたテスト対象コードおよびテストコードをテストハーネスとなるhtmlファイルから参照させ、それを同梱しているPhantomJS 2.1.1上で動作させています。 *4
テストハーネスとなるhtmlファイルは、設定ファイルで指定されたUnitTestエンジンをあらかじめ参照した形で自動的に生成されます。*5この時使用されるUnitTestエンジンは、Chutzpahに同梱されているQUnit 2.4.0、Jasmine 2.5.2、Mocha 3.2.0から選択することになります。*6

事前準備

まずテスト対象のシステムとして、ASP.NET MVCプロジェクトをUnitTest付きで用意しました。

TypeScriptのコードも用意しました。
ChutzpahSample/ChutzpahSample/TypeScript/base/core.ts*7

export class core {
    public static version = 8;
}

ChutzpahSample/ChutzpahSample/TypeScript/ui/ui.ts*8

import { core } from "base/core";

export class ui {
    public static displayVersion = "Version: " + core.version;
}

UnitTestエンジンにはJasmineを選択しました。またビルド時に依存関係を解決するものを用意しなかったので、PhantomJSにはRequireJSで解決してもらうことにしました。なお、これらはnpmで取得しています*9が、JasmineのjsファイルはChutzpahが同梱しているので型定義ファイルだけを取得しています。
ChutzpahSample/ChutzpahSample.Tests/package.json

{
  "name": "chutzpah-sample-test",
  "version": "1.0.0",
  "devDependencies": {
    "@types/jasmine": "~2.5.38",
    "requirejs": "^2.3.5"
  }
}

テストコードですが、Jasmineのjsファイルがビルド時に参照できる場所にないため、Jasmineだけreferenceタグで型定義ファイルを参照させています。
ChutzpahSample/ChutzpahSample.Tests/TypeScript/base/core.spec.ts*10

/// <reference path="../../node_modules/@types/jasmine/index.d.ts" />

import { core } from “../../../ChutzpahSample/TypeScript/base/core”;

describe("base/core", () => {
    it("will return correct version from core", () => {
        let version = core.version;
        expect(version).toBe(8);
    });
});

ChutzpahSample/ChutzpahSample.Tests/TypeScript/ui/ui.spec.ts*11

/// <reference path="../../node_modules/@types/jasmine/index.d.ts" />

import { ui } from "../../../ChutzpahSample/TypeScript/ui/ui";

describe("ui/ui", () => {
    it("will build display version", () => {
        let disp = ui.displayVersion;
        expect(disp).toBe("Version: 8");
    });
});

また、ChutzpahSample.Testsプロジェクト側のTypeScriptのコードがビルドされるように、ChutzpahSampleプロジェクト側の.csprojファイルから下記の2行をChutzpahSample.Testsプロジェクト側の.csprojファイルにコピーしました。

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props" Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')" />

さらに、RequireJSを使うため両プロジェクトのTypeScriptの設定を変更し、モジュールシステムに「AMD」を指定しました。

ちなみに、この状態でビルドを行っても、テストエクスプローラーにはC#のテストしか表示されません。

Chutzpahのインストール

そこでChutzpahをインストールします。Chutzpahのインストールは機能拡張から「Chutzpah Test Adapter for the Test Explorer」を選択し、行います。
f:id:athomeNakajima:20180217124546p:plain

Chutzpahの設定

次にchutzpah.jsonファイルを作成します。設定内容は主に下記の6つです。

  1. UnitTestエンジンにはJasmineを使用しているので、「Framework」を「Jasmine」に。
  2. TypeScriptのコンパイルはChutzpahでは行わずVisual Studio任せにしたいので、「Compile」の「Mode」を「External」に。
  3. テストコードのパスはchutzpah.jsonファイルからの相対パスになるので、「Tests」の「Path」を「TypeScript」*12に。
  4. RequireJSを使用するため、「TestHarnessReferenceMode」を「AMD」に。
  5. 「References」に書くパスを設定ファイルからの相対パスにするため、「TestHarnessLocationMode」を「SettingsFileAdjacent」に。
  6. RequireJSを使用するため、「References」にRequireJSのパス「node_modules/requirejs/require.js」を設定。

ChutzpahSample/ChutzpahSample.Tests/chutzpah.json

{
  "Framework": "jasmine",
  "Compile": {
    "Mode": "External",
    "Extensions": [ ".ts" ],
    "ExtensionsWithNoOutput": [ ".d.ts" ]
  },
  "Tests": [
    {
      "Path": "TypeScript",
      "Includes": [ "*.ts" ]
    }
  ],
  "TestHarnessReferenceMode": "AMD",
  "TestHarnessLocationMode": "SettingsFileAdjacent",
  "References": [
    {
      "Path": "node_modules/requirejs/require.js",
      "IsTestFrameworkFile": true
    }
  ]
}

上記のファイルを保存し、Visual Studioを再起動した後、ビルドを行うとテストエクスプローラーにTypeScriptのUnitTestも表示されるようになります。が、ui.spec.tsにあるUnitTestがまだ表示されません。

これはui.spec.tsとui.tsが異なるプロジェクトにありパスの起点が異なるため、ui.spec.tsから間接的に参照されるcore.tsが探せないためです。そこで、require_config.jsを作成して、参照するパスを変更します。なお、このファイルに記載するパスは、ChutzpahSample.Testsプロジェクトのルートからの相対パスになります。
ChutzpahSample/ChutzpahSample.Tests/TypeScript/require_config.js

require.config({
    paths: {
        "base": "../ChutzpahSample/TypeScript/base",
        "ui": "../ChutzpahSample/TypeScript/ui"
    }
});

そして、このrequire_config.jsをPhantomJSが参照するようにchutzpah.jsonを変更します。*13
ChutzpahSample/ChutzpahSample.Tests/chutzpah.json

{
  "Framework": "jasmine",
  "Compile": {
(中略)
  "References": [
    {
      "Path": "node_modules/requirejs/require.js",
      "IsTestFrameworkFile": true
    },
    {
      "Path": "TypeScript/require_config.js"
    }
  ]
}

これで無事にui.spec.tsにあるUnitTestもテストエクスプローラーに表示されるようになります。

もしうまく行かない場合には、chutzpah.jsonにおいて「EnableTracing」を「true」にし、「TraceFilePath」を「chutzpah.log」と設定すると、chutzpah.jsonと同じフォルダにログが出力されるようになります。特にファイルが見つからないなどのエラーが発生した場合に、どのパスを探しているかを確認する時に重宝します。

まとめ

今回は新規のプロジェクトにChutzpahを導入してみましたが、既存のプロジェクトでも(Visual StudioにChutzpahをインストールしておけば)設定ファイルを書くだけでテストエクスプローラーからJavaScript/TypeScriptのテストが実行できるようになります。
CIサーバーではコマンドラインでの実行が必要なので別の手立てを用意する必要がありますが、コードを書いているときはGUIからC#とTypeScriptのUnitTestを一度に起動できるので、なかなか重宝しています。皆さんも活用してみてはいかがでしょうか。

*1:ブログ記事の数を見ると.NET系が多数派のように見えますが、実は社外向けを含めると少数派だったりします。増やしたい。

*2:ASP.NET Coreの場合は、Visual Studio Codeだったりもします。

*3:.NET系のチームはTypeScriptですが、他のチームはCoffeeScriptだったりES2015だったりします。

*4:このためTypeScriptの出力をES2015(またはそれ以降)にしていると、出力されたJavaScriptをPhantomJSが実行できないため、テストは動作しません。

*5:設定ファイルで指定することもできます。

*6:Jasmineは、1.x系も同梱されています。

*7:このコードはChutzpahのサンプルコードを元にしています。https://github.com/mmanela/chutzpah/blob/master/Samples/RequireJS/TypeScript/base/core.ts

*8:このコードはChutzpahのサンプルコードを元にしています。https://github.com/mmanela/chutzpah/blob/master/Samples/RequireJS/TypeScript/ui/screen.ts

*9:だいたいproxyでやられます。

*10:このコードはChutzpahのサンプルコードを元にしています。https://github.com/mmanela/chutzpah/blob/master/Samples/RequireJS/TypeScript/tests/base/base.qunit.test.ts

*11:このコードはChutzpahのサンプルコードを元にしています。https://github.com/mmanela/chutzpah/blob/master/Samples/RequireJS/TypeScript/tests/ui/ui.qunit.test.ts

*12:通常の設定では、.tsファイルと同じフォルダにも.jsファイルと.js.mapファイルが出力されているため、この設定で動作します。

*13:chutzpah.jsonを書き換えた後は、Visual Studioの再起動が必要です。

ASP.NET Coreの設定変更を即時反映させる

こんにちは、情報システム部の高野です。
だいぶ間が空いてしまいました。継続は中々難しいですね!
最近は、実プロジェクトにべったりなのでHoloLensの検証が一向に進みません。
この記事も実プロジェクトをやっていて気付いたことです。

環境

.Net Core 2.0.5(SDK2.1.4)
ASP.NET Core2.0
テンプレートはMVCで作成

とりあえず普通に設定を利用してみる

ASP.NET Core のオプションのパターン | Microsoft Docs
公式見れば分かります・・・が、一応書いておきます。

設定(appsettings.json)をアプリケーションで利用するには
まず設定を入れておくクラスを作ります。

public class ApiConfig
{
    public string BaseUrl {get; set;}
}


それよりも前にappsettings.jsonに設定の追加が必要ですね。

"HogeApi": {
  "BaseUrl": "http://example.com/api/"
}

これを受けるのが上記のApiConfigクラスになります。


次にStartUpクラスのConfigureServicesメソッド内に下記を追加します。

services.Configure<ApiConfig>(Configuration.GetSection("HogeApi"));


最後に設定を利用したいクラスのコンストラクタの引数にIOptionsを追加します。

ApiConfig config;

public HomeController(IOptions<ApiConfig> option)
{
    config = option.Value;
}

これでHomeController内でURLを取得することができるようになりました。

アプリケーション起動中に設定を変更し即時反映させる

IOptionsを利用すると設定は読み込めるのですが、アプリケーションを再起動しないと
設定の変更が反映されません。

再起動せずに設定を変更させる方法も公式に載っています。
ASP.NET Core のオプションのパターン | Microsoft Docs
ここに載っているそのまま
IOptionsをIOptionsSnapshotに変更すればいいだけです。
(以前は、別途設定が必要だったような気が・・・新しいテンプレートだと無くなってました)

アクションフィルターで設定を利用する

アクションフィルターで設定を用いて処理したいこともありますよね(きっとあるはず)

まずアクションフィルターを作ります。
こちらもコンストラクタを作成して引数でIOptionsを取るようにします。
(即時反映したいならIOptionsSnapshot)

sealed public class TestFilterAttribute : ActionFilterAttribute
{
    ApiConfig config;

    public TestFilterAttribute(IOptions<ApiConfig> option)
    {
        config = option.Value;
    }
}


利用するコントローラーに属性として付与します。
ただしこの場合は、ServiceFilterを利用する必要があります。

[ServiceFilter(typeof(TestFilterAttribute))]
public class HomeController : Controller


StartUpクラスでインジェクションの設定をします。

services.AddScoped<TestFilterAttribute>();


これでアクションフィルターでも設定が利用できるようになったはずです。
上記にも書きましたがIOptionsSnapshotを使用して即時反映も可能です。

ミドルウェアでも設定が使いたい

ミドルウェアでも設定を使いたい時はあるでしょう(あるある)
ということでミドルウェアでもやってみる。

まずミドルウェアを作ります。
これも同じようにIOptionsをコンストラクタの引数で取ります。

public class TestMiddleware
{
    readonly RequestDelegate next;

    ApiConfig config;

    public TestMiddleware(RequestDelegate next, IOptions<ApiConfig> options)
    {
        this.next = next;
        config = options.Value;
    }

    public async Task Invoke(HttpContext context)
    {
        await next.Invoke(context);
    }
}


あとは、StartUpクラスでミドルウェアを登録するだけです。

app.UseMiddleware<TestMiddleware>();


これでミドルウェアでも設定が利用できるようになりました。
即時反映したいならIOptionsSnapshotを使います・・・とはいかないのです!
ミドルウェアのIOptionsをIOptionsSnapshotに変更すると下記のエラーが発生します。

An unhandled exception of type 'System.InvalidOperationException' occurred in Microsoft.AspNetCore.Hosting.dll: 'Cannot resolve scoped service 'Microsoft.Extensions.Options.IOptionsSnapshot`1[OptionsStudy.ApiConfig]' from root provider.'

どうやらScopedで作成したServiceはミドルウェアでは利用できないようです。
IOptionsSnapshotはAddScopedメソッドで登録されています。
IOptionsはAddSingletonメソッドでした。

それでは、ミドルウェアではIOptionsSnapshotは利用できず即時反映はできないのか?
ところができるのです。

コンストラクタの引数を消してInvokeメソッドの方に移すだけでした。
(これが分からず苦労した)

public async Task Invoke(HttpContext context, IOptionsSnapshot<ApiConfig> options)
{
    var conf = options.Value;
   // ・・・
    await next.Invoke(context);
}

まとめ

IOptionsSnapshotをインジェクションすればアプリケーションを再起動しないでも
設定の変更を反映することができる。
でもミドルウェアは、コンストラクタの引数ではなくInvokeメソッドの引数なので注意!




弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp

クリスマス

クリスマスが近づき、街も華やぐ今日この頃、

情報システム部のフロアの一角がクリスマス仕様になりました。

 

f:id:akhr1219:20171207131942j:plain

 

こんにちは 情報システム部の赤堀です。

当記事では、この一角のことやクリスマス準備の風景についてご紹介していきます。

 

 

リフレッシュコーナーができた!

先月フロアのレイアウト変更が行われ、仕事中の息抜きの場となるリフレッシュコーナーができました。

 

ですが!

 

f:id:akhr1219:20171207113337j:plain

机と椅子だけでちょっと寂しい……?

 

f:id:akhr1219:20171207130356j:plain

当部署いやし役のペッパーも、こんな日記を書いちゃう。(※休日だから)

 

 

リフレッシュコーナーを良い感じにしていこう!

リフレッシュコーナーを快適にするため、リフレッシュしやすい環境を作るため、

リフレッシュコーナー運営委員会が発足されました。

 

そして紆余曲折ありまして、委員会活動 第一弾、

リフレッシュコーナーをクリスマス仕様に飾り付けようの会が開かれることになったのです。

 

 

f:id:akhr1219:20171207113249j:plain

委員会メンバーと有志で飾り付けていきます。

 

f:id:akhr1219:20171207113325j:plain

ペッパーも嬉しそう。

 

f:id:akhr1219:20171207113024j:plain

どんどん飾りついていきます。

 

 

 

さて、クリスマスの飾り付けの中にはみんなで手作りした箇所があります。

それは靴下です。

 

f:id:akhr1219:20171207130722j:plain

 

こちらアドベントカレンダー(※日ごとの記事を書かないほう)を模していまして、

赤い靴下がどんどん緑に裏返されていくことでクリスマスまでのカウントダウンを演出しています。

 

靴下の中にはちゃんとお菓子も入ってます。

 

 

ただ靴下のサイズの都合上、お菓子は横から取り出します。

本当に靴下かな?

f:id:akhr1219:20171207113037j:plain

 

 

この靴下は、アドベントカレンダーをみんなで手作りしたい!お菓子を食べたい!

などなどのお話をきっかけに作られました。

普段パソコンばかり眺めているので布を切ったり貼ったりするのは良い気分転換です。

まさにリフレッシュ……?

 

 

でもこれでリフレッシュできたのはまだまだ一部です!

 

より多くの人のリフレッシュを目指して、リフレッシュコーナー運営委員会、今後どんどん活動していきます!

 

 

それではこのあたりで記事を終えたいと思います。

 

 

おわり

RHEL7.4でASP.NET Coreを動かす

こんにちは、情報システム部の高野です。
以前、下記の記事を書きまして今度はRHELだって環境構築したら
またもやハマったので手順を書いておきます。*1
dblog.athome.co.jp

環境

Red Hat Enterprise Linux Server release 7.4 (Maipo)
.Net Core2.0.0 (rhel.7-x64)

.Net Coreのインストール

ここを見れば分かりますが、一応

> sudo subscription-manager repos --enable=rhel-7-server-dotnet-rpms
> sudo yum install scl-utils
> sudo yum install rh-dotnet20
> scl enable rh-dotnet20 bash

※上記は、サーバ版のRHELのコマンドになります。ワークステーション版などはコマンドが違うので注意が必要です。

確認してみる。下記のように表示されればOKです。

> dotnet --info
.NET Command Line Tools (2.0.0)

Product Information:
 Version:            2.0.0
 Commit SHA-1 hash:  cdcd1928c9

Runtime Environment:
 OS Name:     rhel
 OS Version:  7
 OS Platform: Linux
 RID:         rhel.7-x64
 Base Path:   /opt/rh/rh-dotnet20/root/usr/lib64/dotnet/sdk/2.0.0/

Microsoft .NET Core Shared Framework Host

  Version  : 2.0.0
  Build    : N/A


注意が必要なのが、下記のコマンドです。

> scl enable rh-dotnet20 bash

これで「dotnet」コマンドがどこでも起動できるようになるのですが
ターミナルを起動するたびに入力が必要になります。

永続的にコマンドを使いたければ
/etc/profile.d/enabledotnet20.shを作成します。
中身は下記

source scl_source enable rh-dotnet20


ターミナルを再起動せずに利用したい場合は、下記コマンドを実行します。

source /etc/profile.d/enabledotnet20.sh

Nginxのインストール

Nginxのインストールと設定は、以前のCentOSの記事と同様です。

Kestrelをバックグラウンドで実行する

CentOSとパスが違うので記載しておきます。

/etc/systemd/systemに○○.serviceというファイルを作成します。(○○は、なんでもいいです)

[Unit]
  Description=kestrel server
  After=syslog.target network.target
 
[Service]
  ExecStart=/opt/rh/rh-dotnet20/root/bin/dotnet /home/user/www/hoge/sample.dll #dotnetのパスが異なる
  WorkingDirectory=/home/user/www/hoge
  Restart=always
  RestartSec=10
  SyslogIdentifier=sampleapp
  User=user
  Environment=ASPNETCORE_ENVIRONMENT=Staging #サイト毎に環境を変えたい時などに使える
 
[Install]
WantedBy=multi-user.target


起動します。

> sudo systemctl start ○○.service


サーバー再起動時にKestrelも起動したい場合

> sudo systemctl enable ○○.service

デプロイする

dotnet publishコマンドでデプロイ用のモジュールを作成します。

dotnet publish -o /home/www/hoge/ -c Release -r rhel.7-x64

「-r」 オプションで「rhel.7-x64」を付けないとダメです。

まとめ

RHELASP.NET Core環境の作り方を説明しました。
CentOSとは、微妙に違う部分があるので注意が必要です。


弊社ではエンジニアを募集しています。
興味がある方は下記からエントリお願いします。
athome-inc.jp

*1:いつもこのパターンですいません。

バージョン管理システムの比較(という名のポエム)

情報システム部の中嶋です。弊社では以前よりバージョン管理システムVCS)としてGitを採用していますが、Subversion歴が長い*1おっさんとしては、ここで一度ちゃんと比較をしておきたいと思い、4つの論点で比較してみました。(まぁ、控えめに言ってポエムです。)
なお、「チェンジセット記録型」とか「タグ型ブランチ」とかの用語はこの文書のために作った造語なので、あまり気にせずに読み飛ばしていただけると幸いです。

TL;DR

悲観的排他かつ集中リポジトリ 楽観的排他かつ集中リポジトリ 楽観的排他かつ分散リポジトリ
チェンジセット記録型かつコピー型ブランチ VSSなど Subversion、TFVCなど (たぶんない)
スナップショット記録型かつタグ型ブランチ (たぶんない) (たぶんない) Gitなど

「SubversinよりGit」と言う人は、超巨大プロジェクトを扱う機会がなく、マージ先を柔軟に選択できるからGitを選んでいるのではないかと思う。

論点1:悲観的排他と楽観的排他

悲観的排他とは

悲観的排他とは、ファイルの更新をそのファイルの更新権(ロック)を取得してから行うことにより、同時に複数人が同じファイルを更新してしまうことを抑止する方法である。ロックを取得することを「チェックアウト」、更新内容をリポジトリに格納しロックを手放すことを「チェックイン」と呼ぶことが多い。有名なVCSとしては、Visual SourceSafe(VSS)などがある。

楽観的排他とは

楽観的排他とは、「同時に複数人が同じファイルを更新することは稀なので、コンフリクトしたら(自分が修正したファイルを他人も修正してすでにコミットしていたら)その時に考える」という方法である。通常、コンフリクトした場合には、マージを行なってから再度コミットする。一般的に、更新内容をリポジトリに格納することを「コミット」と呼ぶが、リポジトリに格納した更新内容自体をコミットと呼ぶVCSもある。また「チェックアウト」という用語も「作業場の内容を最新に更新する」的な意味で用いられるが、詳細はVCSによって異なる。
近年の楽観的排他を用いたVCSは、コンフリクトが発生した場合にも自動的にマージすることができるようになってきたため、編集者がコンフリクトを解決する場面は少なくなってきている。有名なVCSには、Subversion*2やTeam Foundation Version Control(TFVC)、Gitなどがある。
なお、Subversion*3やTFVCは悲観的排他もサポートしている。

楽観的排他に対する悲観的排他の利点

絶対にコンフリクトが起きないので、マージの必要がない。よって、マージミスは絶対に発生しない。また、Excelファイルなどテキスト形式でないファイルはマージが行えない(もしくは難しい)ため、初めからコンフリクトが起きないようにしておくことは有効な方法である。

悲観的排他に対する楽観的排他の利点

ロックの取得が不要であるため、「他人が更新中であるため、自分が更新できない」という事態が発生しない。また、ロックを取得したまま長期間放置する不心得者への対処が不要である。

論点2:集中と分散

集中リポジトリ型とは

集中リポジトリ型とは、中央にある1つのリポジトリを複数の編集者が共有する方法である。有名なVCSには、VSS、Subversion、TFVCなどがある。

分散リポジトリ型とは

分散リポジトリ型とは、編集者個々がそれぞれリポジトリを所有し、必要に応じてリポジトリ同士を同期させる方法である。一般的には、中央リポジトリを1つ用意し、各編集者はそのリポジトリのコピー(クローン)を手元に作成して使用する。「プッシュ」や「プル」などクローン元のリポジトリと同期するための操作がある。有名なVCSには、Git、Mercurial、BitKeeperなどがある。

分散リポジトリ型に対する集中リポジトリ型の利点

リポジトリをクローンする必要がない。極端に大規模なプロジェクトでなければ問題となることはないが、実際にWindows OSやOfficeなどはリポジトリが巨大であるために分散リポジトリ型への移行が行えずにいた。Microsoftはこの問題を解決するためにGit専用のファイルシステムまで開発することになった*4。また、原理的に悲観的排他は集中リポジトリ型でなければ実現できない*5

集中リポジトリ型に対する分散リポジトリ型の利点

リポジトリは必ず編集者の手元にあるため、オフラインな環境であってもリポジトリにアクセスできる。また、試行錯誤の過程をリポジトリに格納しながら他人には公開しないといったことも行える。
ファイルの更新履歴の表示なども、リポジトリが手元にあるため高速に行うことができる。ただし、ここで表示されるファイルの更新履歴は編集者の手元のリポジトリに記録されているものであるため、必ずしも中央リポジトリと同じとは限らない。

論点3:チェンジセットとスナップショット

チェンジセット記録型とは

チェンジセット記録型とは、コミットの際、リポジトリに現在の状態と直前の状態との変更差分を記録する方法である。リポリトリに格納する操作を「コミットする」、コミットする対象を「チェンジセット」と呼ぶ場合が多い。有名なVCSに、SubversionやTFVCなどがある。
なおVSSも変更差分を記録しているが、同時に行われた複数のファイルに対する変更を1つのものとしては認識せず、ファイル単位で管理している。

スナップショット記録型とは

スナップショット記録型とは、コミットの際、リポジトリに管理対象ファイル全ての現在の状態を記録する方法である。リポジトリに格納する操作も格納する対象も「コミット(する)」と呼ぶ。この方式を採用している代表的なVCSにはGit*6がある。

スナップショット記録型に対するチェンジセット記録型の利点

スナップショット記録型のVCSではファイル名の変更を記録することができない*7。Gitではこの問題を回避するために、「削除されたファイルと追加されたファイルの内容が非常に似ている場合、それはファイル名が変更されたものだ」と判断しUIに表示している。
また、スナップショット記録型は必ず管理対象ファイル全てを同時に扱うため管理対象ファイルの一部だけをチェックアウトすることができないが、チェンジセット記録型では1つのリポジトリのうち必要な部分だけをチェックアウトし作業を行うことができる。このため、チェンジセット記録型はスナップショット記録型よりも大規模プロジェクトに向いていると言える。
さらにスナップショット記録型は各時点でのファイルのスナップショットを保持しているため、チェンジセット記録型よりも格納しているデータ量が多く、特に大きなファイルが存在する場合、リポジトリの肥大化が顕著である。

チェンジセット記録型に対するスナップショット記録型の利点

チェンジセット記録型のVCSでは1つ1つのコミットが変更差分しか持っていないため、1つのコミットだけを独立して操作することはできない。しかしスナップショット記録型のVCSは、そのリポジトリが管理対象としている全てのファイルを1つのコミットの中に保持しているため、1つのコミットだけを独立して操作することができる。これによりコミット順序の変更やコミット単位の調整、分岐元/分岐先以外へのマージが行えるようになっている。

論点4:コピーとタグ

コピー型ブランチとは

コピー型ブランチとは、ブランチの作成を「リポジトリ内でのファイルコピー」として扱い記録する方法である。有名なVCSに、Subversion*8やTFVCなどがある。
なおVSSもコピー型ブランチを採用していると言えるが、ブランチを作成するという行為自体が悲観的排他の利点を損なう行為であるため、利用場面は限られる*9

タグ型ブランチとは

タグ型ブランチとは、ブランチを「コミット指し示すポインタ(ここではタグと表現する)」として扱い記録する方法である。この方式を採用している代表的なVCSには、Git*10がある。

タグ型ブランチに対するコピー型ブランチの利点

コピー型ブランチでは、1つの作業領域に1度に複数のブランチを展開することができる。このため、あるブランチで作業中に他のブランチの作業を行う際、作業中の修正をコミット/退避する必要がない。
また、試験的な実装を行なっている複数ブランチのプログラムを同時に実行するなど、柔軟な対応が行える。これに対し、タグ型ブランチでは1つのブランチしか展開することができない。1つのファイルシステム上に複数のブランチを同時に展開するためには、リポジトリ自体をクローンする必要がある。

コピー型ブランチに対するタグ型ブランチの利点

タグ型ブランチを採用しているVCSにおいて「ブランチの作成」とは、すなわち「タグの作成」であるため、その処理は非常に高速である*11。また、ブランチの内容が作成時に固定化されないため、ブランチの分岐位置を作成後に容易に変更することができる。さらにマージに関しても、マージ先のブランチが指すコミットがマージ元のコミット履歴に含まれる場合はマージ先ブランチが指すコミットを変更するだけで高速かつ確実にマージを行うことができる。


ということで、一通りまとめてみました。後発のシステムほどそれまでのシステムが抱えている問題を解決した良いものになっているわけですが、「人は何かの犠牲なしに何も得ることはできない。」ので多かれ少なかれトレードオフがあったりします。
「じゃぁ、どれを採るか」ということですが、コードレビューをしっかりやるとどうしても変更履歴が無駄に増えてしまうので、ここを変更できるGitが今の所一番使いやすいのかなぁと思っています。


2017年9月8日
公開当初、Mercurialも比較対象に含めておりましたが、内容に誤りがございましたので削除いたしました。

*1:自分は2017年4月にアットホームにJoinしましたが、それまではSubversionやTFVCを使用していました。

*2:http://jtdan.com/vcs/svn/svn-book/svn.basic.vsn-models.html#svn.basic.vsn-models.copy-merge

*3:http://jtdan.com/vcs/svn/svn-book/svn.advanced.locking.html

*4:https://www.infoq.com/jp/news/2017/02/GVFS

*5:Mercurialにはその名もズバリ「Lock Extension」という分散なのに悲観的排他が実現できるエクステンションがあるそうなので、「原理的に〜実現できない」は大げさな表現かもしれません。

*6:https://git-scm.com/book/ja/v1/使い始める-Gitの基本

*7:Subversionも、リポジトリがVer.1.8になるまでは変更が追跡できないことが多かった記憶があります。Ver.1.8になってから、変更の追跡もマージも一気に優秀になりました。

*8:http://jtdan.com/vcs/svn/svn-book/svn.branchmerge.using.html

*9:VSSでも「ファイルの共有」を使用することでバージョン別のブランチを悲観的排他で運用することが可能ですが、マージが前提となるトピックブランチのような運用は想定されていないと思われます。

*10:https://git-scm.com/book/ja/v2/Git-のブランチ機能-ブランチとは

*11:実際にはSubversionにおけるブランチ作成も「このリビジョンからこのブランチを作成した」という情報だけをコミットしているので、そこまでの速度差はないと思われます。

画像システムのリプレイスについて基調講演を行いました

情報システム部の河野です。 入社・開発2年目で、現在はRuby on Rails を使った開発を担当しています。

本日は2017年7月26日(水)に行われました、

ZDNet Japan × TechRepublic Japan & AWS Partner Network 5週間連続セミナーシリーズ データベース編 導入事例から考える–なぜ今、クラウドDBが注目されるのか? の内容について書きたいと思います。

今回このイベントにて、弊社の取り組みについて弊社の鈴木が基調講演をさせて頂きました! 「OracleからAuroraへの移行とオンプレミスとの連携」と題しまして、2016年に行った画像システムのOracle DBのオンプレミス環境からAWSへの移行を行った際の経緯や検討事項、本稼働後の課題についてご紹介しました。

ご存じの方も多いとは思いますが、弊社アットホームは不動産会社と物件を探す方をつなぐ不動産情報サービスを展開しています。 今では物件を探し検討する際、物件の画像がついているのは当たり前とも言える時代です。 弊社の各種サービスでも画像を利用したものは多くあり、その物件画像の配信サービスを支えているシステムは弊社のサービスを支える柱の一つとも言えます。

画像システムの移行と結果

さて、前述にもあります通り画像の需要が増えたことで、画像配信システムのリクエストは1億数千万件/日に上り、キャッシュを利用してもリクエストの件数は増加の傾向にありました。

それにより弊社のシステムには、運用・保守費用の増加や、24時間365日の稼働を行うための安定性確保の課題が浮上してきました。

具体的には

  • Oracleが接続するSANのIO性能の限界
  • ストレージの容量追加やDBサーバのメモリ追加の度に作業が発生(数ヶ月に一度のメンテナンス)
  • ライセンスの上限
  • データ増加に伴いSANの費用が増加(月額課金のサービスを利用)

などです。

そこで2016年7月より、Oracle DBからAmazon Auroraへの移行を行い新システムの運用を開始しました。 弊社がAuroraを採用した理由としては

  • 高可用性
  • 高耐久性
  • 運用の手間が少ない
  • MySQL5.6互換
  • 拡張性

などがあります。

本稼働後の状況

パフォーマンス面では、参照性能はOracle RACとほぼ同等の性能を発揮。大量更新などの際に発生していた、性能劣化が改善しました。

可用性の面では、パラメータチューニングがほとんど不要で、容量追加作業が無くなり、費用面においては、旧システムでかかっていた費用と比較すると半額程度(推定)になりました。

以上のことから、旧システムに比べて随分と楽になった、やってよかった!という結果になりました。

アットホームでは新しい技術を積極的に取り入れるチャレンジをしています。

現在も弊社サービスのリプレイスにて、AWSに移行を伴うプロジェクトも始動中です。

(私もこのプロジェクトに開発メンバーとして参加しています!)

弊社では一緒に開発を行ってくれるエンジニアを募集しています。

興味がある方は下記からエントリお願いします。 ↓↓↓ athome-inc.jp