うさ☆うさ日記 このページをアンテナに追加 RSSフィード

2006 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 02 | 05 | 06 |
2013 | 05 | 08 | 09 | 10 | 11 | 12 |
2014 | 08 |
2015 | 04 | 06 | 08 | 09 | 11 | 12 |
2016 | 01 | 02 | 04 | 05 | 09 | 11 |

2016-11-24

[][]CentOS 7.2上にASP.NET Core Webアプリデプロイしてサービス化する

昨日の続きとして、.NET Core 1.1をインストールしたCentOS上に、Visual Studioで作ったASP.NET Core Webアプリデプロイして、ついでにサービス化する方法について(・ω・)

手順

nginxとの連携とかはせず、ASP.NET Core Webアプリを動かす最低限の作業について記述。

また、アプリビルドCentOS上で行うのではなく、Visual Studio上でビルドしたものをCentOSデプロイするパターンについて記述。

プロジェクト作成

Visual Studioで「ASP.NET Core Web Application (.NET Core)」を作成。

1.1へ更新

Visual Studioで作成されるひな形は1.0用の構成。

CentOSには1.1が入っているので1.1用に更新。

まずはNuGetですべてのパッケージを更新。


次に、project.jsonの次の箇所を更新。

  • 変更前
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },
  • 変更後
  "frameworks": {
    "netcoreapp1.1": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },

また、次の箇所も変更。

  • 変更前
    "Microsoft.NETCore.App": "1.1.0",
  • 変更後
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },

project.json全体は以下のような内容。

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Diagnostics": "1.1.0",
    "Microsoft.AspNetCore.Mvc": "1.1.0",
    "Microsoft.AspNetCore.Razor.Tools": {
      "version": "1.0.0-preview2-final",
      "type": "build"
    },
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
    "Microsoft.Extensions.Configuration.Json": "1.1.0",
    "Microsoft.Extensions.Logging": "1.1.0",
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.Extensions.Logging.Debug": "1.1.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.1.0",
  },

  "tools": {
    "BundlerMinifier.Core": "2.2.306",
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
  },

  "frameworks": {
    "netcoreapp1.1": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },

  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },

  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },

  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },

  "scripts": {
    "prepublish": [ "bower install", "dotnet bundle" ],
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}
URL設定

デフォルトではlocalhostからのアクセスのみ許可されるので、外部からも接続できるようにUseUrls()の記述を追加。

public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseUrls("http://*:80/")
        .UseStartup<Startup>()
        .Build();

    host.Run();
}

なお、この時点でVisual Studio上でもデバッグ動作できることを確認しておく。

できなければ何かが不足している状態。

デプロイ

今回はLinux上でビルドするのではなく、開発環境上でビルドしたものを配布する。


Visual Studioの[ビルド]メニューからアプリケーションの発行を実施。

発行方法は「ファイルシステム」で、ターゲットの場所はデフォルトだと「.\bin\Release\PublishOutput」。

上記フォルダの内容がCentOS上へコピーするファイル。


なお、発行と同じことをコマンドラインからMSBuildで行う場合、以下のコマンドで指定したディレクトリにファイルを作成可能。

MSBuild src\Xxx\Xxx.xproj /p:Configuration=Release /t:PackagePublish /p:PublishOutputPathNoTrailingSlash=bin\Release\PublishOutput
Firewall設定

CentOSに対して外部から接続可能にするためFirewallを設定。

[root@centos ~]# firewall-cmd --zone=public --add-port=http --permanent
success
[root@centos ~]# firewall-cmd --reload
success
[root@centos ~]#
動作確認

CentOS上のコピー先は「/opt/webapp」として、そこに移動してdotnetコマンドを実行。

[root@centos ~]# cd /opt/webapp
[root@centos webapp]# dotnet WebApplication.dll
Hosting environment: Production
Content root path: /opt/webapp
Now listening on: http://*:80
Application started. Press Ctrl+C to shut down.

この状態で、外部からWebブラウザで動作を確認する。

問題がなければ画面が表示され、コンソールにもログが表示される。

サービス化

ついでに上記アプリケーションのサービス化。


まず、起動用のスクリプトとして以下のような/opt/webapp.shを作成。

#!/bin/bash

cd /opt/webapp
dotnet WebApplication.dll

実行権限の設定。

[root@centos ~]# chmod 755 /opt/webapp.sh

次に、サービスの起動スクリプトとして以下のような/etc/systemd/system/webapp.serviceを作成。

[Unit]
Description = webapp daemon

[Service]
ExecStart = /opt/webapp.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

以下のコマンドで正しく作成されていることを確認。

[root@centos ~]# systemctl list-unit-files --type=service | grep webapp
webapp.service                              enabled

サービスの開始。

[root@centos ~]# systemctl start webapp

これで、OS再起動してもWebアプリケーションが起動。

なお、これはあくまで最低限の設定なので、実際にプロダクションに導入する際には、もう少しスクリプトの内容を検討すること。


…っということで、これで実行環境がLinuxだからという理由でJavaでWebアプリを作っていたような人達も、.NET Coreで同程度のことはできるようになったわけで、これまで.NETのウィークポイントであった実行環境面が克服されたのでした٩(๑´3`๑)۶

みんな、これからはWebアプリASP.NET Coreで作るのじゃ(`・ω・´)

2016-11-23

[][]CentOS 7.2上に.NET Core 1.1をインストールする

昨日に続き、Dockerではなく、CentOSに直接インストールする方法について(・ω・)

動作環境などは昨日に引き続きで。

手順

.NET Core installation guideに従ってやるだけ。

CentOS以外は、ディストリビューション毎の記述を参照のこと。

必要ライブラリインストール
[root@centos ~]# yum install -y libunwind libicu
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
 * base: ftp.riken.jp
 * extras: ftp.riken.jp
 * updates: ftp.riken.jp
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ libicu.x86_64 0:50.1.2-15.el7 を インストール
---> パッケージ libunwind.x86_64 2:1.1-5.el7_2.2 を インストール
--> 依存性解決を終了しました。

依存性を解決しました

======================================================================================================================================================================================================
 Package                                        アーキテクチャー                            バージョン                                             リポジトリー                                  容量
======================================================================================================================================================================================================
インストール中:
 libicu                                         x86_64                                      50.1.2-15.el7                                          base                                         6.9 M
 libunwind                                      x86_64                                      2:1.1-5.el7_2.2                                        updates                                       56 k

トランザクションの要約
======================================================================================================================================================================================================
インストール  2 パッケージ

総ダウンロード容量: 6.9 M
インストール容量: 24 M
Downloading packages:
(1/2): libunwind-1.1-5.el7_2.2.x86_64.rpm                                                                                                                                      |  56 kB  00:00:00
(2/2): libicu-50.1.2-15.el7.x86_64.rpm                                                                                                                                         | 6.9 MB  00:00:03
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
合計                                                                                                                                                                  1.9 MB/s | 6.9 MB  00:00:03
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  インストール中          : 2:libunwind-1.1-5.el7_2.2.x86_64                                                                                                                                      1/2
  インストール中          : libicu-50.1.2-15.el7.x86_64                                                                                                                                           2/2
  検証中                  : libicu-50.1.2-15.el7.x86_64                                                                                                                                           1/2
  検証中                  : 2:libunwind-1.1-5.el7_2.2.x86_64                                                                                                                                      2/2

インストール:
  libicu.x86_64 0:50.1.2-15.el7                                                                    libunwind.x86_64 2:1.1-5.el7_2.2

完了しました!
ダウンロード及び展開
[root@centos ~]# curl -sSL -o dotnet.tar.gz https://go.microsoft.com/fwlink/?LinkID=835019
[root@centos ~]# mkdir -p /opt/dotnet && sudo tar zxf dotnet.tar.gz -C /opt/dotnet
[root@centos ~]# ln -s /opt/dotnet/dotnet /usr/local/bin
動作確認
[root@centos ~]# dotnet

Microsoft .NET Core Shared Framework Host

  Version  : 1.1.0
  Build    : 928f77c4bc3f49d892459992fb6e1d5542cb5e86

Usage: dotnet [common-options] [[options] path-to-application]

Common Options:
  --help                           Display .NET Core Shared Framework Host help.
  --version                        Display .NET Core Shared Framework Host version.

Options:
  --fx-version <version>           Version of the installed Shared Framework to use to run the application.
  --additionalprobingpath <path>   Path containing probing policy and assemblies to probe for.

Path to Application:
  The path to a .NET Core managed application, dll or exe file to execute.

If you are debugging the Shared Framework Host, set 'COREHOST_TRACE' to '1' in your environment.

To get started on developing applications for .NET Core, install the SDK from:
  http://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409
[root@centos ~]# dotnet --info
.NET Command Line Tools (1.0.0-preview2-1-003177)

Product Information:
 Version:            1.0.0-preview2-1-003177
 Commit SHA-1 hash:  a2df9c2576

Runtime Environment:
 OS Name:     centos
 OS Version:  7
 OS Platform: Linux
 RID:         centos.7-x64

っで、次回はVisual Studio 2015で作ったASP.NET Core Webアプリケーションをこの環境で動かす話について٩(๑>◡<๑)۶

2016-11-22

[][]CentOS 7.2上にSQL Server on Linuxインストールする

Dockerではなく、CentOSに直接インストールする方法について(・ω・)

動作環境

OSインストール後、yum updateしたところまでの状態からはじめる。

手順

Install SQL Server on Red Hat Enterprise Linuxに従ってやるだけ。

RHELインストール手順だが、CentOSも同じでOK。

repository configuration取得
[root@centos ~]# curl https://packages.microsoft.com/config/rhel/7/mssql-server.repo > /etc/yum.repos.d/mssql-server.repo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   220  100   220    0     0    101      0  0:00:02  0:00:02 --:--:--   101
インストール
[root@centos ~]# yum install -y mssql-server
読み込んだプラグイン:fastestmirror
packages-microsoft-com-mssql-server                                                                                                                                                    | 2.9 kB  00:00:00
packages-microsoft-com-mssql-server/primary_db                                                                                                                                         | 2.4 kB  00:00:00
Loading mirror speeds from cached hostfile
 * base: ftp.tsukuba.wide.ad.jp
 * extras: ftp.tsukuba.wide.ad.jp
 * updates: ftp.tsukuba.wide.ad.jp
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ mssql-server.x86_64 0:14.0.1.246-6 を インストール
--> 依存性の処理をしています: bzip2 のパッケージ: mssql-server-14.0.1.246-6.x86_64
--> 依存性の処理をしています: gdb のパッケージ: mssql-server-14.0.1.246-6.x86_64
--> トランザクションの確認を実行しています。
---> パッケージ bzip2.x86_64 0:1.0.6-13.el7 を インストール
---> パッケージ gdb.x86_64 0:7.6.1-80.el7 を インストール
--> 依存性解決を終了しました。

依存性を解決しました

==============================================================================================================================================================================================================
 Package                                      アーキテクチャー                       バージョン                                     リポジトリー                                                         容量
==============================================================================================================================================================================================================
インストール中:
 mssql-server                                 x86_64                                 14.0.1.246-6                                   packages-microsoft-com-mssql-server                                 138 M
依存性関連でのインストールをします:
 bzip2                                        x86_64                                 1.0.6-13.el7                                   base                                                                 52 k
 gdb                                          x86_64                                 7.6.1-80.el7                                   base                                                                2.4 M

トランザクションの要約
==============================================================================================================================================================================================================
インストール  1 パッケージ (+2 個の依存関係のパッケージ)

総ダウンロード容量: 140 M
インストール容量: 145 M
Downloading packages:
(1/3): bzip2-1.0.6-13.el7.x86_64.rpm                                                                                                                                                   |  52 kB  00:00:00
(2/3): gdb-7.6.1-80.el7.x86_64.rpm                                                                                                                                                     | 2.4 MB  00:00:03
warning: /var/cache/yum/x86_64/7/packages-microsoft-com-mssql-server/packages/mssql-server-14.0.1.246-6.x86_64.rpm: Header V4 RSA/SHA256 Signature, key ID be1229cf: NOKEY=-] 2.7 MB/s | 140 MB  00:00:00 ETA
mssql-server-14.0.1.246-6.x86_64.rpm の公開鍵がインストールされていません
(3/3): mssql-server-14.0.1.246-6.x86_64.rpm                                                                                                                                            | 138 MB  00:01:07
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
合計                                                                                                                                                                          2.1 MB/s | 140 MB  00:01:07
https://packages.microsoft.com/keys/microsoft.asc から鍵を取得中です。
Importing GPG key 0xBE1229CF:
 Userid     : "Microsoft (Release signing) <gpgsecurity@microsoft.com>"
 Fingerprint: bc52 8686 b50d 79e3 39d3 721c eb3e 94ad be12 29cf
 From       : https://packages.microsoft.com/keys/microsoft.asc
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  インストール中          : bzip2-1.0.6-13.el7.x86_64                                                                                                                                                     1/3
  インストール中          : gdb-7.6.1-80.el7.x86_64                                                                                                                                                       2/3
  インストール中          : mssql-server-14.0.1.246-6.x86_64                                                                                                                                              3/3

+-------------------------------------------------------------------+
| Please run /opt/mssql/bin/sqlservr-setup to complete the setup of |
|                  Microsoft(R) SQL Server(R).                      |
+-------------------------------------------------------------------+

  検証中                  : mssql-server-14.0.1.246-6.x86_64                                                                                                                                              1/3
  検証中                  : gdb-7.6.1-80.el7.x86_64                                                                                                                                                       2/3
  検証中                  : bzip2-1.0.6-13.el7.x86_64                                                                                                                                                     3/3

インストール:
  mssql-server.x86_64 0:14.0.1.246-6

依存性関連をインストールしました:
  bzip2.x86_64 0:1.0.6-13.el7                                                                            gdb.x86_64 0:7.6.1-80.el7

完了しました!
セットアップ

パスワードはポリシーが有効になっているので、ある程度複雑なものが必要。

後でSSMS上から変更可能。

[root@centos ~]# /opt/mssql/bin/sqlservr-setup
Microsoft(R) SQL Server(R) Setup

You can abort setup at anytime by pressing Ctrl-C. Start this program
with the --help option for information about running it in unattended
mode.

The license terms for this product can be downloaded from
http://go.microsoft.com/fwlink/?LinkId=746388 and found
in /usr/share/doc/mssql-server/LICENSE.TXT.

Do you accept the license terms? If so, please type "YES": YES

Please enter a password for the system administrator (SA) account:
Please confirm the password for the system administrator (SA) account:

Setting system administrator (SA) account password...

Do you wish to start the SQL Server service now? [y/n]: y
Do you wish to enable SQL Server to start on boot? [y/n]: y
Created symlink from /etc/systemd/system/multi-user.target.wants/mssql-server.service to /usr/lib/systemd/system/mssql-server.service.
Created symlink from /etc/systemd/system/multi-user.target.wants/mssql-server-telemetry.service to /usr/lib/systemd/system/mssql-server-telemetry.service.

Setup completed successfully.
状態確認
[root@centos ~]# systemctl status mssql-server
● mssql-server.service - Microsoft(R) SQL Server(R) Database Engine
   Loaded: loaded (/usr/lib/systemd/system/mssql-server.service; enabled; vendor preset: disabled)
   Active: active (running) since 火 2016-11-22 21:01:21 JST; 12s ago
 Main PID: 2810 (sqlservr)
   CGroup: /system.slice/mssql-server.service
           tq2810 /opt/mssql/bin/sqlservr
           mq2823 /opt/mssql/bin/sqlservr

1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.37 spid17s     Server is listening on [ 0.0.0.0 <ipv4> 1433].
1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.38 Server      Server is listening on [ 127.0.0.1 <ipv4> 1434].
1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.38 Server      Dedicated admin connection support was established for listening locally on port 1434.
1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.38 spid17s     SQL Server is now ready for client connections. This is an informational message; no user action is required.
1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.57 spid7s      Starting up database 'tempdb'.
1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.98 spid7s      The tempdb database has 1 data file(s).
1122 21:01:26 centos sqlservr[2810]: 2016-11-22 12:01:26.99 spid20s     The Service Broker endpoint is in disabled or stopped state.
1122 21:01:27 centos sqlservr[2810]: 2016-11-22 12:01:26.99 spid20s     The Database Mirroring endpoint is in disabled or stopped state.
1122 21:01:27 centos sqlservr[2810]: 2016-11-22 12:01:27.02 spid20s     Service Broker manager has started.
1122 21:01:27 centos sqlservr[2810]: 2016-11-22 12:01:27.09 spid5s      Recovery is complete. This is an informational message only. No user action is required.
Firewall設定
[root@centos ~]# firewall-cmd --zone=public --add-port=1433/tcp --permanent
firewall-cmd --reloadsuccess
[root@centos ~]# firewall-cmd --reload
success
接続

SQL Server Management Studio (SSMS) のダウンロードからダウンロードしたSSMSで接続可能。

16.5では未サポートの動作あり。

データベース作成

デフォルトでは[照合順序]が「SQL_Latin1_General_CP1_CI_AS」になるので変更して作成する。*1

また、ユーザの[規定の言語]も「Arabic」になっているので変更しておく。


次回はCentOS上に.NET Core 1.1をインスコする話について(๑˃̵ᴗ˂̵)و

*1:Japanese_XJIS_140_*とかで

2016-11-14

[]ASP.NET CoreアプリDependency Injection処理を別のDIコンテナに委譲する(オマケ) Request Scopeへの対応

昨日の日記の続きとして、ついでにRequest Scopeでのオブジェクト管理機能を追加してみます(・ω・)


通常、各種DIコンテナのスコープ管理については、その実装固有の部分があるため、詳細な対応方法は異なります。

ここでは、ASP.NET Coreとの統合をするにあたり、どのような初期化処理や終了処理が必要かについてサンプルでその概要を示し、他のDIコンテナでも応用できるようにします。

サンプル

昨日のサンプルにRequest Scopeの対応を追加してあります。

今回は上記からソースを抜粋して概要を記述することで対応方法について示します。

概要

Smart.Resolverについて

Request Scopeの実装ではコンテナ固有の知識が必要になるため、まずSmart.Resolverのスコープ管理の概要について記述しておきます。

Smart.Resolverは、標準ではPrototypeSingleton Scopeにのみ対応したDependency Resolverです。

ただし、機能の拡張が可能となっており、以下の実装を用意することで独自のスコープ管理機能を追加することが可能になっています。

インターフェース 概要
IScope IScopeStorageのファクトリー、Bind()時に指定する
IScopeStorage Remember()/TryGet()/Clear()を実装する、スコープ内のオブジェクトプール機能

独自のスコープ管理を行いたい場合には、IScopeStorageの実装とそのファクトリーであるIScopeの実装を用意します。

Resolverに対してインスタンス要求が行われた場合、スコープ管理されているバインディングについては、まずIScopeStorage.TryGet()でスコープのオブジェクトプールからインスタンスの検索を行います。

また、オブジェクトプールにインスタンスがない場合はインスタンスを生成し、IScopeStorage.Remember()によりオブジェクトプールにインスタンスを追加するというのがSmart.Resolverのスコープ管理の概要です。


なお、Singleton Scopeでのスコープ管理も、Dictionaryを使用したIScopeStorageにより実装されています。


今回は、HttpContext.Itemsでオブジェクトを管理するRequestScopeStorageを用意することで、Request Scopeでのオブジェクト管理機能を追加しています。

ライフサイクル管理

Request Scopeでのライフサイクル管理の要件を要約すると、HTTPリクエストが終わったタイミングでオブジェクトプール内のインスタンス破棄を行う、ということになります。

これをASP.NET Coreのコードイメージで言えば、以下のような処理を書けば良いと言うことになります。

app.Use(async (context, next) =>
{
    using (new RequestScopeObjectPool())
    {
        await next();
    }
});

サンプルでは以下のような拡張メソッドを用意して、next()の後でRequest Scopeで管理されるオブジェクトの破棄を行うようにしています。

public static void UseSmartResolverRequestScope(this IApplicationBuilder app, StandardResolver resolver)
{
    var storage = new RequestScopeStorage(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>());

    // RequestScopeStorageでのオブジェクト管理機能をStandardResolverに追加
    resolver.Configure(container => container.Register(storage));

    app.Use(async (context, next) =>
    {
        try
        {
            await next();
        }
        finally
        {
            // Request Scopeで管理されるオブジェクトの破棄
            storage.Clear();
        }
    });
}

利用方法としては、以下のようにStartupにおいてこの拡張メソッドを以下のように使用することで、MVCの処理が終わった後にオブジェクトの破棄処理ができるようになります。

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
    // UseMvcの前に記述
    app.UseSmartResolverRequestScope(resolver);

    // Enable request scope
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

なお、注意事項として、UseSmartResolverRequestScope()はUseMvc()の前に記述する必要があります。

HttpContextでのオブジェクト管理

Request Scopeオブジェクト自体はHttpContext.Itemsに入れてライフサイクル管理を行いますが、ASP.NET CoreではHttpContextへのアクセスにIHttpContextAccessorを使用します。

IHttpContextAccessorは標準ではサービスに追加されていないので、Startupで以下のようにIHttpContextAccessorの追加をしておく必要があります。

public void ConfigureServices(IServiceCollection services)
{
...
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
...
}

これにより、拡張メソッドUseSmartResolverRequestScope()内において、IApplicationBuilder.ApplicationServices.GetRequiredService<IHttpContextAccessor>()でIHttpContextAccessorの取得が可能となります。

RequestScopeStorage内ではIHttpContextAccessor.HttpContextにより現在のHttpContextを取得し、そこでオブジェクトプールを管理しています。

この詳細についてはRequestScopeStorageのソースを参照してください。

バインディング

Request Scopeで管理されるオブジェクトバインディングは以下のように記述します。

resolver
    .Bind<RequestScopedObject>()
    .ToSelf()
    .InRequestScope();

上記のバインディングを記述することにより、RequestScopedObjectのインスタンスはHttpContext内で管理されるようになり、HTTPの処理毎にインスタンスの生成と破棄が行われるようになります。

なお、InRequestScope()は統合用に用意した拡張メソッドで、その実態は以下のようになっています。

public static IBindingNamedWithSyntax InRequestScope(this IBindingInSyntax syntax)
{
    return syntax.InScope(new RequestScope());
}

以上が、Request Scopeへの対応の概要となります。


まあ、自分はRequest Scopeが必要になるような設計をすることは無いと思うんですけどね(´д`;)

2016-11-13

[]ASP.NET CoreアプリDependency Injection処理を別のDIコンテナに委譲する

ASP.NET CoreではDependency Injectionが標準機能として組み込まれていますが、それでは機能不足な事も多いと思います(´・ω・`)

そこで、他のDIコンテナASP.NET Coreを連携させる方法について記述します。


通常、各種DIコンテナについては、その拡張としてASP.NET Coreとのインテグレーション機能が提供されると思いますが、今回は自作のDependency Resolverを使用して、連携方法の仕組み自体について記述します。

この方法と同じやり方をすることで、インテグレーション機能が提供されていないコンテナについても、ASP.NET Coreとの連携が可能となります。

環境

手順

IControllerActivator実装

IControllerActivatorの派生クラスとして、以下のような実装を用意します。

なお、IResolverはSmart.ResolverのDependency Resolverインターフェースになります。

using System;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;

using Smart.Resolver;

public class SmartResolverControllerActivator : IControllerActivator
{
    private readonly IResolver resolver;

    public SmartResolverControllerActivator(IResolver resolver)
    {
        this.resolver = resolver;
    }

    public object Create(ControllerContext context)
    {
        return resolver.Get(context.ActionDescriptor.ControllerTypeInfo.AsType());
    }

    public void Release(ControllerContext context, object controller)
    {
        (controller as IDisposable)?.Dispose();
    }
}
Startup修正

StartupクラスでStandardResolverをメンバに定義し、ConfigureServices()でSmartResolverControllerActivatorの設定を行います。

using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;

using Smart.Resolver;

public class Startup
{
    private readonly StandardResolver resolver = new StandardResolver();
...
    public void ConfigureServices(IServiceCollection services)
    {
        // 標準のASP.NET設定はここ

        services.AddSingleton<IControllerActivator>(new SmartResolverControllerActivator(resolver));
    }
...
}

IControllerActivatorが設定されると、コントローラーのインスタンスはその実装経由で行われるようになります。

この例では、実際のインスタンスの生成をSmart.ResolverのDependency Resolver実装であるStandardResolverに委譲することで、ASP.NET CoreとSmart.Resolverの連携を実現しています。

サンプル

実際に動くサンプルを以下に用意しました。

このサンプルは、用途毎に複数のDB接続があるようなアプリケーションを想定したものです。

接続を生成する複数のIConnectionFactoryのインスタンスについて、ASP.NET Coreの標準ではサポートされない条件付きバインディングを行っています。

なお、SQLiteとDapperを使用し、実際にデータアクセスまで行っています。

インスタンス生成委譲対象

サンプルで、StandardResolverにインスタンスの生成/管理を委譲する主なクラスについて以下に記述します。

クラス スコープ 概要
CharacterController Prototype CharacterServiceを使用するAPIコントローラー
ItemController Prototype ItemServiceを使用するAPIコントローラー
CharacterService Singleton 名称"Character"のCallbackConnectionFactoryを使用する
ItemService Singleton 名称"Master"のCallbackConnectionFactoryを使用する
IConnectionFactory Singleton * 2 "Character"、"Master"の2つのインスタンスが存在する
Resolver初期化コード

Resolverの初期化コードを以下に抜粋します。

var connectionStringMaster = Configuration.GetConnectionString("Master");
resolver
    .Bind<IConnectionFactory>()
    .ToConstant(new CallbackConnectionFactory(() => new SqliteConnection(connectionStringMaster)))
    .Named("Master");
var connectionStringCharacter = Configuration.GetConnectionString("Character");
resolver
    .Bind<IConnectionFactory>()
    .ToConstant(new CallbackConnectionFactory(() => new SqliteConnection(connectionStringCharacter)))
    .Named("Character");

resolver
    .Bind<MasterService>()
    .ToSelf()
    .InSingletonScope()
    .WithConstructorArgument("connectionFactory", kernel => kernel.Get<IConnectionFactory>("Master"));
resolver
    .Bind<CharacterService>()
    .ToSelf()
    .InSingletonScope()
    .WithConstructorArgument("connectionFactory", kernel => kernel.Get<IConnectionFactory>("Character"));

CallbackConnectionFactoryについては、接続情報の異なる2つのインスタンスを、Named()メソッドにより異なる名称で登録しています。

2つのServiceクラスについては、Singletonスコープとして登録し、インスタンス生成時のコンストラクタ引数connectionFactoryについて、名称指定でResolverから取得して設定するようにしています。

なお、Named()及びWithConstructorArgument()によって条件付きバインディングを行っていますが、名称による条件付きバインディングはNamedAttributeを使う事でも可能です。


また、Smart.Resolverでは明示的に情報を登録しないクラスについてはPrototypeスコープとして扱われるため、コントローラーについてはResolverへの情報登録は不要となっています。


ちなみに、IControllerActivatorの他に、IViewComponentActivatorとかもありますでよ(・ω・)

2016-11-12

ASP.NET CoreなWebアプリで起動時にコントローラーの一覧を取得する

IApplicationBuilderから以下のような感じで一覧のTypeInfoを取得できます(・ω・)

public static void EnumControllers(IApplicationBuilder app)
{
    var manager = app.ApplicationServices.GetRequiredService<ApplicationPartManager>();

    var feature = new ControllerFeature();
    manager.PopulateFeature(feature);

    foreach (var typeInfo in feature.Controllers)
    {
        System.Diagnostics.Debug.WriteLine(typeInfo.AsType());
    }
}

HomeController、ValuesControllerの2つのコントローラーがあったとして、出力結果としては以下のような感じになります。

WebApplication1.Controllers.HomeController
WebApplication1.Controllers.ValuesController

起動時に、コントローラーのメタデータからなにかを構築したり、DIコンテナに明示的に情報を登録したり、みたいなことをしたい時用に(・ω・)

2016-11-11

[]ASP.NET Core 1.0なWebアプリでStyleCopを使う

project.jsonさっさとタヒネ、っという挨拶からはじまって、StyleCopを使用する方法についてメモ(・ω・)


project.jsonに以下のエントリを追加してStyleCop.Analyzersを追加。

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    },
...
    "StyleCop.Analyzers": {
      "version": "1.0.0",
      "type": "build"
    }
  },

StyleCopが使用するrulesetについては、project.jsonのbuildOptionsにadditionalArgumentsを追加して以下のように設定。

  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true,
    "additionalArguments": [ "/ruleset:Hoge.ruleset" ]
  },

ところで、ASP.NET Core 1.0なWebアプリでコード分析(FxCop)を簡単に有効化する方法はないんかいのう(´・ω・`)

2016-09-20

[]TSqlParserをテストのモックと組み合わせてみた

出発点

SQL Serverでは、T-SQLのパーサーとジェネレーターがMicrosoft.SqlServer.TransactSql.ScriptDom名前空間のクラスとして、.NETのライブラリで提供されていますが。

例えば、パーサーであるTSqlParser(具象クラスはSQL Serverのバージョンに応じたTSql130Parserとか)を使って、SQLの検証を行うことができます。

そこで、Micro-ORMとSQL記述による開発などで、SQL記述の部分の誤りを、これを利用することで改善できるのではないかと思うものの、以外と使いどころが難しいなどと思ったりもして(´・ω・`)

その課題と、今回やってみたことについて書いてみます。

課題

課題は、検証の対象とするSQLをどう判断するかという部分。

ソース中に記述されたSQL文字列について、Roslynを使って抽出して、TSqlParserでコンパイルタイムで問題を検出、…とかできると良いんですが、これはなかなか厳しそう(´・ω・`)

文字列を検出したとして、それがSQLかそうでないのかを判断する基準がないですし、SQL文字列が動的に組み立てられているケースでは、文字列自体はフラグメントにすぎなかったりするので。

SQL文字列をファイル化(外部化)しているようなケースであれば、その判断は容易にできますが、そもそもファイル化されているのであればエディタの検証とかで済みそうと言う話もあり。


っということで、コンパイルタイムでの問題検出はあきらめ、別途思いついたのが、テスト時にMockと組み合わせる方法です(・ω・)

やってみたこと

なにができるようになったかについて、ライブラリにまとめてNuGetからも落とせるようにしているので、先にその確認から。

まず、テストプロジェクトを作って、NuGetで「Usa.Smart.Mock.Data.SqlServer」とテストコード用に「Dapper」を追加します。

っで、テストとしては以下のようなものを用意します。

using Dapper;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using Smart.Mock.Data;
using Smart.Mock.Data.SqlServer;

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        using (var connection = new MockDbConnection())
        {
            connection.SetupCommand(cmd => cmd.SetupResult(1));

            connection.Execute("UPDATE Employee SET Name = @Name WHERE Id = @Id", new { Id = 1234, Name = "うさ☆うさ" });

            var result = connection.ValidateSql();
            Assert.IsTrue(result.Valid, result.ToString());
        }
    }
}

まず、これを実行するとテストは成功します。


次に、connection.Execute()部分をで「WHERE」を「WH ERE」のように書き換えて実行してみると、

Assert.IsTrue に失敗しました。Error [46010] (Line = 1, Column = 34) : 'WH 付近に不適切な構文があります。'

のように、テストに失敗し、SQLの問題箇所も出力されることを確認できましたヽ( ・ω・)ノ

解説

まず、NuGetでUsa.Smart.Mock.Data.SqlServerを追加すると、依存関係で自動的に追加されるMicrosoft.SqlServer.TransactSql.ScriptDomとUsa.Smart.Mock.Dataについて説明しておきます。


Microsoft.SqlServer.TransactSql.ScriptDomは、NuGetで配布されているScriptDomのアセンブリです。


Usa.Smart.Mock.Dataはモック用に自前で用意したライブラリであり、IDbConnection、IDbCommand、IDataReader等のモックを用意することで、本物のDBを使用せずに、あたかもDBの処理が正常終了したかのように結果を上位に返すためのものです。

このモックを使うことで、データアクセス層やいわゆるRepository*1のような部分でインターフェースの分離とモックの作成を行うのではなく、エッジ(DB接続)部分までは本物のコードを使ったテストができるようになります。


そして、今回のメインはValidateSql()を実装するSmart.Mock.Data.SqlServerライブラリになります。

ValidateSql()の実装は、Usa.Smart.Mock.Dataが提供するモッククラス(MockDbConnection等)に対する拡張メソッドとして用意してあり、中身はTSqlParser.Parse()をラッピングしているだけです(・∀・;)


まあ、詳細はこの辺から。

https://github.com/usausa/Smart-Net-Mock-Data/blob/master/Smart.Mock.Data.SqlServer/Data/SqlServer/MockExtensions.cs

結論

っということで、本物のDBを使用せず、モックで済ましてテストする場合についても、併せてSQLの構文チェックができるようになったのでした。

LocalDBとかを使っても、環境面や速度面での面倒さはあるし、ロジックのテストだからといってモックだけで済まそうとすると、SQLの構文誤りに気がつかない、なんてこともあり得るので、少しは役に立つのかな(・ω・)?

*1:余談だけど、ドメインモデルのSource/Sink的な性格を持つものではなく、単にデータアクセスをしている層をなんでもRepositoryって銘々するの、ワイは違和感あるんやで(´・ω・`)

2016-09-17

[]作って理解するDependency Resolver(なぜかDIコンテナと言わないというこだわり)

f:id:machi_pon:20160917184055p:image:right

出発点

今更だけど、Compact Framework*1でもDependency Resolverが使いたくなったので、Ninjectのデッドコピーを組んでみたのですよ(・ω・)

先に.NET 4.6.2/PCL版を作って、そこからCompact Frameworkに移植したわけですが、せっかくなのでその実装について書いてみます。

なお、「Dependency Resolver(意地でもDIと言わない)の何がいいの?」については語るつもりはねーですよ。


とりあえずソースはこちら。

https://github.com/usausa/Smart-Net-Resolver

NuGetでも取得できますのだ。

https://www.nuget.org/packages/Usa.Smart.Resolver

何ができるん?

まだるっこしいんで、サンプルコードを先に。*2

public interface IService
{
}

public class Service : IService
{
}

public class Controller
{
    public IService Service { get; }

    public Controller(IService service)
    {
        Service = service;
    }
}

// Web的なイメージのテストコード
using (var resolver = new StandardResolver())
{
    resolver.Bind<IService>().To<Service>().InSingletonScope();
    resolver.Bind<Controller>().ToSelf();

    var controller1 = resolver.Get<Controller>();
    var controller2 = resolver.Get<Controller>();

    Assert.AreNotSame(controller1, controller2);
    Assert.AreSame(controller1.Service, controller2.Service);
}

っで、コードを見た後で、何ができるのかについて書いておきます。

  • Guice型のDependency Resolverです
  • バインディングの情報を登録しておき、その情報に基づいて依存関係を解決しながらオブジェクトを生成し、その取得が可能です
  • 実装している機能は、本家Ninjectと比較した場合、自分が使いそうなもののみにバッサリ省略しています

Resolver(コンテナ)に登録するのはあくまでバインディングの情報であり、インスタンスを単にDictionaryにプールするだけのなんちゃって実装ではないです。

依存関係の解決も、コンストラクタインジェクションを基本として再帰的にインスタンスの生成を行います(・∀・)

Binding

バインディングシンタックスは下記のようなものをサポートしています(・ω・)

  • Bind<T>()

バインディング情報を作成します。

  • To<TImplementation>()

バインディングを指定された型に関連づけます。

// インターフェース型の要求に対してその実測クラスのインスタンスを返す
resolver.Bind<IService>().To<Service>();
  • ToSelf()

バインディングを自身の型に関連づけます。

// 具象クラスの要求に対してそのインスタンスを返す
resolver.Bind<Controller>().ToSelf();
  • ToMethod(Func<IKernel, T> factory)

バインディングに対して生成するインスタンスのファクトリーメソッドを指定します。

// 型の要求に対して、そのファクトリーをResolverから取得して、そのメソッドでインスタンスを生成する
// この例のケースでは、ISchedulerFactoryに対するバインド情報も登録しておく
resolver.Bind<IScheduler>().ToMethod(_ => _.Get<ISchedulerFactory>().GetScheduler());
  • ToConstant(T value)

バインディングに対して固定のインスタンスを指定します。

// 型の要求に対してその固定のインスタンスを返す
resolver.Bind<Messenger>().ToConstant(Messenger.Default);
  • ToProvider(IProvider provider)

バインディングに対してインスタンスを返すIProviderを指定します。

IProviderの定義は以下のような形で、他のTo系メソッドの中身は、処理に応じたIProviderを指定しているだけのものです。

public interface IProvider
{
    object Create(IKernel kernel, IBinding binding);
}

To系メソッドと使用されるIProvider実装は以下のようになっています。

メソッドIProvider実装概要
ToConstant()ConstantProvider指定された固定値を返す
ToMethod()CallbackProviderFunc<IKernel, T>を評価した結果を返す
To()、ToSelf()StandardProviderコンストラクタ情報から依存性を解決してインスタンスを生成して返す
  • InTransientScope()

バインディングで生成されるインスタンスを揮発性にします。

要は、ライフサイクル管理をなにもしませんのだ(・ω・)

Scope指定が省略された場合のデフォルト動作になります。

// Controllerは毎回異なるインスタンスが生成される
resolver.Bind<Controller>().ToSelf().InTransientScope();
  • InSingletonScope()

バインディングで生成されるインスタンスをシングルトンで管理します。

このスコープで管理されたオブジェクトがIDisposableを実装する場合、ResolverのDispose()時にオブジェクトのDispose()も呼び出されます。

// 要求に対して毎回同じServiceのインスタンスが返される
resolver.Bind<IService>().To<Service>().InSingletonScope()
  • InScope()

ライフサイクル管理の方法(IScope実装)を指定します。

IScopeインターフェースは実際にライフサイクル管理を行うIScopeStorageのファクトリで、InSingletonScope()についてもResolver自体は特殊な扱いをせず、この機構によって実現しています。

IScopeStorageの定義は以下のような形で、InSingletonScope()で使用される実装は、Dictionaryによる単純なオブジェクト管理をしているだけのものになります。

public interface IScopeStorage
{
    void Remember(IBinding binding, object instance);

    object TryGet(IBinding binding);

    void Clear();
}
  • Named(string name)

同じ型に対する複数のバインディングを構築する際に、名前を与えることでそれらを区別できるようにします。

参照する側は、NamedAttribute属性を使うことでインジェクションされるオブジェクト選択できます。

// 名前で識別してインジェクションされる
public class Controller
{
    public Controller([Named("master")] Service service)
    {
...
    }
}

// 名前で識別して登録
resolver.Bind<IService>().ToConstant(new Service("master")).InSingletonScope().Named("master");
resolver.Bind<IService>().ToConstant(new Service("slave")).InSingletonScope().Named("slave");

// masterの方が解決されたものが取得できる
var controller = resolver.Get<Controller>();
  • WithConstructorArgument(string name, object value)、WithConstructorArgument(string name, Func<IKernel, object> factory)

型からインスタンスを生成する場合、通常はコンストラクタの各引数再帰的にResolverで解決されますが、一部の引数の値をバインディング時に指定できます。

// ITimerは再帰的にResolverから取得されるが、timeoutには30が設定される
public class Sceduler
{
    public Sceduler(ITimer timer, int timeout)
    {
    }
}

resolver.Bind<ITimer>().To<Timer>().InSingletonScope();
resolver.Bind<Sceduler>().ToSelf().InSingletonScope().WithConstructorArgument("timeout", 30);

何用かというと、コードでバインディング情報を書くのではなく、設定ファイルから機械的バインディング情報を作る際に使用するケースを主に想定しています。

  • WithPropertyValue(string name, object value)、WithPropertyValue(string name, Func<IKernel, object> factory)

WithConstructorArgument()のプロパティインジェクションバージョンです。

プロパティインジェクションについてはまた後で(☆ゝω・)b⌒☆

  • WithMetadata(string key, object value)

バインディングに対して任意のメタデータを設定します。

メタデータは何に使うかというと、主に型に対して複数のバインディング情報があるケースで、どのバインディングを使用するかの制約条件で使用します。

制約の話についてはまた後で(・ωー)〜☆

Resolve

Resolverからインスタンスを取得するためのインターフェースとしては、以下の形で1件用と複数件用のものを用意しています。

public interface IResolver
{
    object Resolve(Type type, IConstraint constraint);

    IEnumerable<object> ResolveAll(Type type, IConstraint constraint);
}

また、使いやすくするために、以下の処理を拡張メソッドとして用意しています。

T Get<T>(this IResolver resolver);

T Get<T>(this IResolver resolver, string name);

IEnumerable<T> GetAll<T>(this IResolver resolver);

object Get(this IResolver resolver, Type type);

object Get(this IResolver resolver, Type type, string name);

IEnumerable<object> GetAll(this IResolver resolver, Type type);

っで、IResolverの所に出てきているIConstraintとはなんぞや(・ω・)?、っという話がありますが。

これが、同じ型に対して複数のバインディングがあった際に、使用するバインディング選択するための制約で、絞り込み条件のパラメータクラスにようなものになります。


例えば、Named()により異なる名称のバインディングが登録されている場合に、名前に一致する方のバインディングからオブジェクトを生成するためにはstring nameが引数にある拡張メソッドを使えば良いですが、その中身は以下のような感じになっています。

public static T Get<T>(this IResolver resolver, string name)
{
    return (T)resolver.Resolve(typeof(T), new NameConstraint(name));
}

その他

バインディング情報の登録と、インスタンスの取得という基本機能以外の諸々についてです。

複数件をコレクションで取得

例えば、

public class Client
{
    // 登録されているITimer全件を配列で欲しい
    public Client(ITimer[] timers)
    {
...
    }
}

resolver.Bind<ITimer>().ToConstant(new Timer(30)).InSingletonScope().Named("timer1");
resolver.Bind<ITimer>().ToConstant(new Timer(60)).InSingletonScope().Named("timer2");
resolver.Bind<ITimer>().ToConstant(new Timer(120)).InSingletonScope().Named("timer3");

var client = resolver.Get<Client>();

のようなケースへの対応についてですが。


単純にTypeをキーにして処理する場合、Resolverに登録されているのはtypeof(ITimer)の情報なので、typeof(ITimer[])とは異なるわけで(´・ω・`)

例えば、WithConstructorArgument()の機能を使って、

resolver.Bind<Client>().ToSelf().WithConstructorArgument(_ => _.GetAll<ITimer>());

っと書けば、それでも解決できますが、いちいちそう書くのも面倒です(´・ω・`)

っということで、引数の型が配列の場合や、GetGenericTypeDefinition()がIEnumerable<>、ICollection<>、IList<>の場合には特別扱いして、その型の配列を設定するようになっています。*3

プロパティインジェクション

Immutableの観点で、コンストラクタインジェクションだけあれば、まあ、困らないのが通常ですが。

ただ、既存のライブラリとの折り合いが悪いケースとかも想定して、プロパティインジェクションも一応用意しておくのが大人の対応というもの(`・ω・´)

public class Client
{
    [Inject]
    public Service Service { get; set; }
}

上記のように、InjectAttributeがついているプロパティについては、インスタンス生成後に参照を設定してくれる動作になっています。

また、StandardResolverにはInject(object value)メソッドも用意してあり、これはResolverの管理外の既存のオブジェクトに対して、上記と同じ処理を行うメソッドになっています。


インジェクション機能はパイプラインで管理され、標準では、実装としてPropertyInjectorクラスのみが用意してありますが、下記のインターフェースの実装を追加することで、他の方法によるインジェクションにも対応できる構造になっています。

public interface IInjector
{
    void Inject(IKernel kernel, IBinding binding, TypeMetadata metadata, object instance);
}
オブジェクトアクティベーション

インスタンス生成後に、PostConstruct的なものを呼んで欲しいニーズへ対応するための機能です。

これもプロパティインジェクションなどと同様にパイプライン処理になっており、下記のインターフェースの実装を追加することで、生成されたインスタンスに対して各種処理を呼び出すフックポイントを用意してあります。

public interface IActivator
{
    void Activate(object instance);
}

標準では、実装としてInitializeActivatorクラスのみが用意してあり、この処理では対象となるinstanceがIInitializableインターフェースを実装している場合、IInitializable.Initialize()を呼び出す、っという処理になっています。

制約

同じ型に対する複数のバインディング情報に対して、その識別をするための機能です。

これもまだるっこしいので、先にソースから。

例として、バインディング情報のメタデータにキーとなる文字列を設定し、そのキーを制約条件として絞り込むIConstraint実装を作る例は以下のようになります。

// 制約条件
public class HasMetadataConstraint : IConstraint
{
    public string Key { get; }

    public HasMetadataConstraint(string key)
    {
        Key = key;
    }

    public bool Match(IBindingMetadata metadata)
    {
        return metadata.Has(Key);
    }
}

// 2件のバインディングを作成
resolver.Bind<Target>().ToConstant(new Target("default"));
resolver.Bind<Target>().ToConstant(new Target("hoge")).WithMetadata("hoge", null);

// メタデータにhogeがある方を取得
var hoge = resolver.Resolve(typeof(Target), new HasMetadataConstraint("hoge"));

また、NamedAttributeのように、属性で制約条件を指定する場合は以下のようにConstraintAttribute派生を作成して使用します。

// 制約条件の属性
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class HasMetadataAttribute : ConstraintAttribute
{
    public string Key { get; }

    public HasMetadataAttribute(string key)
    {
        Key = key;
    }

    public override IConstraint CreateConstraint()
    {
        return new HasMetadataConstraint(Key);
    }
}

// 使用例
public class HasMetadataConstraintInjectedObject
{
    public Target Target { get; }

    public HasMetadataConstraintInjectedObject([HasMetadata("hoge")] Target target)
    {
        Target = target;
    }
}

var obj = resolver.Get<HasMetadataConstraintInjectedObject>();

Named()メソッドおよびNamed属性による制約も、基本的にはこの仕掛けによって実現しています。

Namedに関する処理で異なるのは、Namedだけは頻出パターンということで、通常のメタデータKey/ValueのDictionaryで管理しているのに対し、メタデータに専用のプロパティを用意している点と、バインディング構築の専用シンタックスを用意している点だけです。


まあ、拡張できる仕組みはあるものの、Namedだけあれば困ることもないと思いますが(´・_・`)

デフォルトバインディング

Resolverを通じて生成するオブジェクトについて、必ずバインディングの情報の登録をする必要があるとすると面倒です。*4

そこで、バインディングが未登録の場合に、IBindingResolver実装を用意することで、自動でバインディングを構築する仕掛けがあります。

標準ではSelfBindingResolverクラスが用意してあり、バインディング情報が見つからない場合には、下記の記述と同じバインディングが構築されるようになっています。

resolver.Bind<Target>().ToSelf();

また、この機能もパイプラインで管理されており、IBindingResolver実装を用意することで、例えば、他のフレームワーク組み込みのセンスの悪いstaticなDependencyServiceみたいなもの*5があったとして、このResolverで解決できない場合にはそっちに解決を移譲、みたいな仕組みを作ることが可能になっています。

カスタマイズ

ここまで、パイプラインや拡張できる仕組みについての記述がありましたが、Resolverの各機能はサブモジュールとして構築されており、それを変更することで動作のカスタマイズが可能になっています。

サブモジュールはResolverが持つマイクロコンテナで管理されており、Configure()メソッドによってその登録情報の変更が可能です。

// カスタムIActivatorの追加
resolver.Configure(c => c.Get<IActivatePipeline>().Activators.Add(new CustomInitializeActivator()));

// カスタムIBindingResolverの追加
resolver.Configure(c => c.Get<IMissingPipeline>().Resolvers.Add(new CustomBindingResolver()));

// アクティベートおよびインジェクションパイプラインの完全無効化
// IInitializableやプロパティインジェクションが無効になる代わりに性能があがる
resolver.Configure(c => c.Remove<IActivatePipeline>());
resolver.Configure(c => c.Remove<IInjectPipeline>());

この考えも本家由来になりますが、バッサリ簡略化して、マイクロコンテナについてはDictionary型のインスタンスベースにコンテナになっています。


ちなみに、最近、こういう風に、コンポーネントのサブモジュールをマイクロコンテナに登録しておき、コンポーネント本体は軽量にしてサブモジュールに処理を委譲、マイクロコンテナに登録されているサブモジュール情報を変更することでコンポーネントの挙動変更が可能、っという作りが好みだったりしています(・∀・)

何ができないの?

このDependency Resolverがサポートしない/するつもりもない機能について、なぜサポートしていないのかと、仮にサポートするとしたらどうなのかとかについて、ちょっと書いてみたり。

AOP( ゚д゚)、ペッ

知らない子ですね( ˘ω˘ )


世の中にはAOPのためのDIみたいな事を言い出す人もいて困りものですが、自分が欲しいのはあくまで「Dependency Resolver」ですので。

そもそも、AOPを何に使うかといえば、トランザクション、ログ、認証あたりをあげる人が多いと思いますが、

  • トランザクション制御はLoanパターンとか、明示的なスコープで制御しろ
  • ログや認証は明示的なフィルタ機構を使ってやれ、フレームワークがその機構を持っていないならそいつが糞なだけ、AOPで無理矢理穴をあけるようなものじゃない*6

っということで論破です(フンスフンス


まあ、もし仮に、本当に仮にAOPしたくなったとしたら、IProxyGeneratorインターフェース作って、StandardProviderの最後でそれをかますとかですが。

その前に、どういうシンタックスで指定したいかの検討からかな(・ω・)?

メソッドインジェクション

これはまあ、単純に自分が使うケースがないから実装してないだけですが(・ω・)

やるとしたら、PropertyInjectorと同様にMethodInjectorを作ってInjectPipelineに使うようにするだけですけど。

ただ、そもそもイラネーやということで、内部で使ってる型のメタデータや、InjectAttributeやNamedAttributeといった属性などについて、メソッドに関する部分を削除してしまっているので、もしMethodInjectorを実装するなら、その辺も修正した方がよくはあります。

循環参照の検知

検知機能を入れる場合には、HashSetなんかを使って型に対する処理の再実行を判断する仕組みを入れることになりますが。

ただ、Resolverを使う側の、オブジェクト間の構成の設計に問題がなければ無駄っちゃ無駄なので、省略しています(゚Д゚;)

リクエストスコープ

Webアプリケーションの場合に、リクエスト毎のライフサイクル管理を行いたいようなケースについてですが。

自分が、あんまりそういう使い方をすることがないので、実装していません(`・ω・´)

IScope/IScopeStorage実装を作る(実装自体は単純なDicitionaryでよく、リクエスト終了時にIScopeStorage.Clear()を呼び出すフィルタ機構を用意すればよい)ことで対応は可能ですが、どのみちWeb固有の話なので、コア機能に入れるようなものではなく、別アセンブリに分ける形になりますが。


っで、リクエストスコープの概念を導入する場合に、1点問題になるのが、より広いスコープのオブジェクトからリクエストスコープのオブジェクトに対する参照がある場合にどうするかという点。

例えば、シングルトンスコープのオブジェクトがリクエストスコープのオブジェクトを参照するような場合にどうするのか?。


まあ、んなものサポートしねーヨ、でもいいと思いますが。

これをどうしてもやりたい場合、ここに来てやっと、否定しまくったAOPというかプロキシーの生成機能が必要になってきます(´・ω・`)

ランタイムでのリクエストスコープのインスタンスを参照するようなプロキシーを作って、本来のオブジェクトの参照の代わりにそのプロキシーをシングルトンスコープのオブジェクトにインジェクションするような形ですね。*7

バインディングの自動登録/設定ファイルからの構築

設定ファイルからの構築については、コアの機能としては作っていないですが、拡張機能として作るつもりもあったりもして。

WithConstructorArgument()の実装を用意したのはそのためというのもあるし(・ω・)


自動登録については、そもそも、Guice型ではそんなもんイラネーヨというのがまずあるし、やるにしても登録ルールの指定とか面倒になるだけだしね〜、という話もありますが。

なによりも、自動登録が必要だと思うと言うことは、どーせ馬鹿みたいに(というか馬鹿だから)、なんにでもインターフェースと実装を用意するような、知能レベルを疑う設計をしているんでしょ?、っという話もあったりして(・∀・;)


ちなみに、本来どこについてIFと実装を分離すべきかについては、よくあるWebシステムみたいなものではなくて、もっといろんな形態のシステムを作った方がわかるようになると思いますです。

実装に依存して責務が明確になっていない設計も悪です、「疎結合は善」という言葉を免罪符にしてなんにでもIFを導入するのも同レベル悪です、思考停止という意味において。

まとめ、組んでみた感想

俺にはこういうのでいいんだよ、こういうので( ˘ω˘ )


今まで、Dependency Resolverの実装について、特に拘りはないとか言っていましたが、実際に自分が必要な機能を考えて組んでみたら、この程度の機能は必要だということになり、単純Dictionary型では事足りないということになりまスタ。

っと言っても、本家Ninjectから比べれば機能は絞りまくっていて、ソースは全体で1kくらいしかないので、お手軽感もあって、自分にはちょうどいい感じです。

サイズと機能面のバランスから、DI的なものの学習用とにも使えるかしら(・ω・)?


ちなみに、性能面についても、割とどうでもいいのですが、一応ふれておきます。

まず、単純なDictionary型よりは当然遅いです。

単純Dictionaryタイプのものに比べれば、そこそこ機能もあり拡張可能にしているので、どうしたってオーバーヘッドは入りますし、マイクロベンチをとればその差は大きく見えたりもします。

そもそも、機能比較の併記がない性能比較に意味はないので、それらとの比較に価値なんてないんですが、自分の用途や、どれくらいの処理オーダーを想定するのかの立脚点がないと、そういうものに惑わされることもありますし(´・ω・`)


一方で、本家から比べた場合には、本家はもっといろいろやっているので、それに比べればこのResolverは骨しか残っていないようなものなので、当然速くはなりますが。

もっとも、自分が本気で高性能なDependency Resolverが必要になったら、オブジェクトをnewするメソッドが並ぶコードを生成する、T4Resolver(イメージとしては以下)とかを用意すると思いますが٩(๑•ㅅ•๑)و

// Auto generated.
private static readonly Lazy<IService> service = new Lazy<IService>(() => new Service());

private static IService GetService()
{
    return service.Value;
}

private static Controller GetController()
{
    return new Controller(GetService());
}

...

っということで、以上が今回の学習内容となります(`・ω・´)ゞ

組んだライブラリの話に見せかけた、ある種の設計に関するdisでした(?)

チッ、反省してま〜す。

*1:ぎょーむ端末の世界ではまだ普通に使われているゾイ(´・ω・`)

*2:ちなみに、この例では解説用にいわゆるサービス層的なものをインターフェースと実装にわけてるけど、ワイはがなんでもIF/実装小僧だとは思わないでね(´・ω・`)

*3:ちなみに、本家ではジェネリッククラスのオープン構築型/クローズ構築型あたりの扱いとかについても、もう少しちゃんとした実装が入っていたりするんですが、こいつは「仕様です」もしくは「そんな使い方はするな」の精神で、そのへんはあまり考慮していません(・ω<)

*4:後述しますが、Guice型の場合自動登録とかするのもチト思想が違うので

*5:おっと、それ以上は言うなよ…、なんのためにPCL版も用意したと思ってるんだ?

*6:まして、パッチ用途に使うみたいなアンチパターンとか…

*7:Springとかは、そんなことやってたと思う

2016-05-10

[]SensorTagをNode.jsで扱う(私家版)

SensorTagをNode.jsで扱うサンプルについて、見かけるサンプルの多くが複数のSensorTagを想定していなかったり、SensorTagのON/OFFをしたさいの再接続に対応していなかったので、対応した版を書いてみました。

IoTごっことかをする場合、アプリは常駐したままでSensorTagのON/OFFをするだろうし、複数のSensorTagを扱えないと実用的ではないと思うので(´・ω・`)


とりあえずソース。

var SensorTag = require('sensortag');

var discovering = false;

// センサー設定
function setupSensor(sensorTag) {
  sensorTag.discoverServicesAndCharacteristics(function() {
    // 温度
    sensorTag.enableIrTemperature(function() {});
    sensorTag.on('irTemperatureChange', function(objectTemperature, ambientTemperature) {
      var data = {};
      data.timestamp = new Date().getTime();
      data.uuid = sensorTag.uuid;
      data.messsage = 'temperature';
      data.objectTemperature = objectTemperature.toFixed(1);
      data.ambientTemperature = ambientTemperature.toFixed(1);

      console.log(JSON.stringify(data));
    });
    sensorTag.notifyIrTemperature(function() {})

    // 湿度
    sensorTag.enableHumidity(function() {});
    sensorTag.on('humidityChange', function(temperature, humidity) {
      var data = {};
      data.timestamp = new Date().getTime();
      data.uuid = sensorTag.uuid;
      data.messsage = 'humidity';
      data.temperature = temperature.toFixed(1);
      data.humidity = humidity.toFixed(1);

      console.log(JSON.stringify(data));
    });
    sensorTag.notifyHumidity(function() {})
  });
};

// センサー探索
function discoverSensor() {
  // discoverの重複実行はしない
  if (discovering) {
    return;
  }
  discovering = true;

  console.log('start discover:');

  // 探索開始
  SensorTag.discover(function(sensorTag) {
    discovering = false;

    console.log('discovered: ' + sensorTag.uuid + ', type = ' + sensorTag.type);

    // 接続開始
    sensorTag.connect(function() {
      console.log('connected: ' + sensorTag.uuid);

      sensorTag.once('disconnect', function() {
        console.log('disconnected: ' + sensorTag.uuid);

        // 探索再開
        discoverSensor();
      });

      setupSensor(sensorTag);

      // 探索再開
      discoverSensor();
    });
  });
};

// 開始
setInterval(function() {
  discoverSensor();
}, 10000);

discoverSensor();

ポイントはSensorTag.discover()の実行タイミングくらい(・ω・)?

通知周りについては、SensorTagを検知したらセンサーを有効にして後はSensorTagに任せるモデルなので、バッファリングや外部への通信については後はconsole.log(JSON.stringify(data))のタイミングで弄るようにしてくださいな。