ASP.NET Core MVC - オプションパターンで設定ファイルを読み込む

オプションパターンを使って設定ファイルから構成情報を読み込むサンプルです。

こちらを読みながら試しました。

ASP.NET Core のオプション パターン | Microsoft Docs

上記ドキュメントにもメリットが書いてありますが、平たく言ってしまうと「オプションパターンを使うと、設定を機能ごとにグルーピングしてわかりやすく管理できますよ」ってことかなと。

ということでコードを見ていきましょう。

次の設定ファイル(appsettings.json)をコントローラやビューで読み込んでみたいと思います。

// appsettings.json
{
    "App": {
        // ある機能の設定だと思ってもらえると
        "Sample": {
            "Value1": 10,
            "Value2": 20
        }
        // 他の機能の設定はここに足していく感じで
    }
}

設定をバインドするクラス

まずはappsettings.jsonSample以下をバインドするクラスを用意します。

// appsettings.jsonのSample以下をバインドするクラス
public class SampleSettings {
    public int Value1 { get; set; }
    public int Value2 { get; set; }
}

設定をバインドできるようにする

設定を上記クラスにバインドできるように(DIで取得できるように)するために、オプションをサービスに登録します。

具体的にはStartup.ConfigureServicesメソッドを次のようにします。

public class Startup {
    private readonly IConfiguration _config;

    public Startup(IConfiguration config) {
        _config = config;
    }

    public void ConfigureServices(IServiceCollection services) {
        services.AddMvc();

        // 設定をクラスにバインドできるようにする
        services.Configure<SampleSettings>(_config.GetSection("App:Sample"));
        // この書き方でもいいみたい
        //services.Configure<SampleSettings>(_config.GetSection("App").GetSection("Sample"));
    }

    // 略
}

コントローラで設定を参照する

コントローラで設定を参照してみましょう。

コンストラクタインジェクションでIOptions<TOptions>インターフェイスを指定すると、 SampleSettingsを取得できるようになります。

public class DefaultController : Controller {
    private readonly SampleSettings _settings;

    // DIでIOptions<SampleSettings>を取得
    public DefaultController(IOptions<SampleSettings> options) {
        // IOptions.ValueプロパティからSampleSettingsを取得できる
        _settings = options.Value;
    }

    public IActionResult Index() {
        // 設定を参照できる
        Console.WriteLine($"{nameof(SampleSettings.Value1)}: {_settings.Value1}");
        Console.WriteLine($"{nameof(SampleSettings.Value2)}: {_settings.Value2}");
        // Value1: 10
        // Value2: 20

        return View();
    }
}

ビューで設定を参照する

ビューで設定を参照するには@injectを使います。

@using Microsoft.Extensions.Options;
@inject IOptionsSnapshot<SampleSettings> Options

@{
    var settings = Options.Value;

    @* 設定を参照できる *@
    <div>@nameof(SampleSettings.Value1): @settings.Value1</div>
    <div>@nameof(SampleSettings.Value2): @settings.Value2</div>
    @*
        <div>Value1: 10</div>
        <div>Value2: 20</div>
    *@
}

設定を再読込する

IOptionsの代わりにIOptionsSnapshotを使うと、Webアプリケーション実行中に変更したappsettings.jsonの値を再読み込みできるようになります。

public class DefaultController : Controller {
    private readonly SampleSettings _settings;

    // IOptionsの代わりにIOptionsSnapshotを使うと再読み込みできる
    public DefaultController(IOptionsSnapshot<SampleSettings> options) {
        _settings = options.Value;
    }
}

ドキュメントによると、リクエストごとに1回読み込んでキャッシュされるっぽいです。

ASP.NET Core 2.0 以降では、オプションは、要求の有効期間中にアクセスされ、キャッシュされたとき、要求につき 1 回計算されます。

引用元

おしまい。

ASP.NET Core MVC - グローバルフィルタで認証

グローバルフィルタを使って認証を必要とする方法です。認証を必要とするというかアクセス制御するというか。

ウェブアプリのうちログインを必要とする部分が多くてログイン不要な部分が少ない場合、グローバルフィルタで全体を制御しつつ、ログインなしで利用したいコントローラ・アクションにAllowAnonymous属性をつけると手堅いかなと思います。

Core MVCでは、ConfigureServicesメソッド内でAddMvcメソッドを使ってサービスを追加するときに、AuthorizeFilterを作成してMvcOptions.Filtersに追加すればいいみたいです。

public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        // クッキー認証を行うためにサービスを登録
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();

        services.AddMvc(options => {
            // ウェブアプリ全体で認証を必要にするため、グローバルフィルタに追加
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        });
    }
}

CoreではないMVCとかなり違ったので悩みました。たぶんこれでいいはず。

参考

T-SQL - lag関数/lead関数でn個前/n個後の行を取得する

lag関数とlead関数を知ったのでメモしておきます。

それぞれ一言で説明すると、

  • lag関数 ... n個前の行を取得する
  • lead関数 ... n個後の行を取得する

といった感じです。この2つもウィンドウ関数と言っていいのかな。

構文の簡単なパターンだと、関数の1つ目の引数には「取得するカラム名」を、2つ目の引数には「n個前/n個後」を指定します。

lag(カラム名, n個前) over(...)
lead(カラム名, n個後) over(...)

年ごとや四半期ごとの比較といった実用的なサンプルは上記ドキュメントを参照してもらうとして、 ここでは超簡単なサンプルクエリを書いて実行結果を残しておきます。

with Seq(Value)  -- CTE(共通テーブル式)
as(
    -- 1から10まで連番を作る
    select 1
    union all
    select Seq.Value + 1
    from Seq
    where Seq.Value + 1 <= 10
)
select
    Value,
    -- Valueを昇順に並べ替えて3つ前の行を取得
    lag(Value, 3) over(order by Value) as Lag,
    -- Valueを昇順に並べ替えて3つ後の行を取得
    lead(Value, 3) over(order by Value) as Lead
from Seq
order by Value;

/*
Value       Lag         Lead
----------- ----------- -----------
1           NULL        4
2           NULL        5
3           NULL        6
4           1           7
5           2           8
6           3           9
7           4           10
8           5           NULL
9           6           NULL
10          7           NULL
*/

結果セットを見ると例えばValueが4の場合に、lag関数は1、lead関数は7を取得できていることがわかります。

また、lag関数/lead関数を使わないで上記と同じ結果を取得するには、 次のような自己結合を使ったクエリになると思いますが、 lag/lead関数を使ったほうがわかりやすいかなと。

-- lag関数/lead関数の代わりに自己結合を使って、3つ前、3つ後の行を取得する
with Seq(Value)
as(
    select 1
    union all
    select Seq.Value + 1
    from Seq
    where Seq.Value + 1 <= 10
)
select
    Src.Value,
    Dst1.Value as Lag,
    Dst2.Value as Lead
from Seq as Src
    left outer join Seq as Dst1
        on Src.Value = Dst1.Value + 3
    left outer join Seq as Dst2
        on Src.Value = Dst2.Value - 3
order by Src.Value;

T-SQL - count(*)とcount(カラム名)の違い

知らなかったのでメモ。

count(*)は全行数を取得するのに対して、count(カラム名)はnullを除く行数を取得します。

サンプルクエリを書いて確認しておきましょう。

select
    -- nullに関係なく全行数を取得
    count(*) as [count(*)],
    -- nullを除く行数を取得
    count(Value) as [count(Value)]
from (values
    (1, 100),
    (2, 200),
    (3, 200),
    (4, null),
    (5, 300),
    (6, 200)) as Test(Id, Value);

-- 行は全部で6、nullを除くと5
/*
count(*)    count(Value)
----------- ------------
6           5
*/

おまけ

countだけでなくsumやavgなどの関数もnullを無視して集計します。

select
    count(Value) as [count(Value)],
    sum(Value) as [sum(Value)],
    avg(Value) as [avg(Value)]
from (values
    (1, 100),
    (2, 200),
    (3, 200),
    (4, null),
    (5, 300),
    (6, 200)) as Test(Id, Value);

/*
count(Value) sum(Value)  avg(Value)
------------ ----------- -----------
5            1000        200
*/

おまけ2

nullの行しか存在しない場合はどうなるんだろうと思って、countやsumなどの関数を試しました。次のような結果になります。

select
    count(Value) as [count(Value)],
    sum(Value) as [sum(Value)],
    avg(Value) as [avg(Value)],
    min(Value) as [min(Value)],
    max(Value) as [max(Value)]
from (values
    -- nullの行が1つ
    (cast(null as int))) as Test(Value);

/*
count(Value) sum(Value)  avg(Value)  min(Value)  max(Value)
------------ ----------- ----------- ----------- -----------
0            NULL        NULL        NULL        NULL
警告: NULL 値は集計またはその他の SET 演算で削除されました。
*/

ん?警告?

参考

COUNT (Transact-SQL) | Microsoft Docs

ASP.NET Core - ミドルウェアパイプラインを作ってみる

ミドルウェアとかミドルウェアパイプラインとかリクエストデリゲートなどを理解するために、次のドキュメントを読みながら記事内にあるミドルウェアパイプラインの画像の動きを作ってみました。

ASP.NET Core のミドルウェア | Microsoft Docs

この画像のことです。

https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/index/_static/request-delegate-pipeline.png

Use/Run拡張メソッドを使ってパイプラインを作る

Startup.Configureメソッド内でミドルウェアパイプラインを構築します。Use拡張メソッドやRun拡張メソッドを使うと、ミドルウェアをインライン(ラムダ式)で記述できます。Use拡張メソッドは次のミドルウェアコンポーネントとも言ったりしてる)を呼び出しますが、Run拡張メソッドは次のミドルウェアを呼び出さずパイプラインの終端になります。

public class Startup {
    public void ConfigureServices(IServiceCollection services) {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        // 最初に呼び出されるミドルウェア
        app.Use(async (context, next) => {
            context.Response.ContentType = "text/plain";

            // 次を呼び出す前の処理
            await context.Response.WriteAsync("Middleware 1 before\n");

            // 次のミドルウェアを呼び出す
            await next.Invoke();

            // 次を呼び出した後の処理
            await context.Response.WriteAsync("Middleware 1 after\n");
        });

        // 2つ目のミドルウェア
        app.Use(async (context, next) => {
            await context.Response.WriteAsync("\tMiddleware 2 before\n");

            await next.Invoke();

            await context.Response.WriteAsync("\tMiddleware 2 after\n");
        });

        // 最後に呼び出されるミドルウェア
        app.Run(async context => {
            await context.Response.WriteAsync("\t\tMiddleware 3\n");
        });
    }
}

実行すると次のレスポンスが返ります。

Middleware 1 before
    Middleware 2 before
        Middleware 3
    Middleware 2 after
Middleware 1 after

ミドルウェアクラスとUseMiddleware拡張メソッドを使ってパイプラインを作る

上記と同じことをクラスを使って再現したいと思います。インラインでの処理をミドルウェアクラスにカプセル化します。

ミドルウェアクラスはHttpContextを引数に持つInvokeAsyncメソッドを持っていればいいみたいです。

// 最初に呼び出されるミドルウェア
public class Middleware1 {
    private readonly RequestDelegate _next;

    public Middleware1(RequestDelegate next) {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context) {
        context.Response.ContentType = "text/plain";

        // 次を呼び出す前の処理
        await context.Response.WriteAsync("Middleware 1 before\n");

        // 次のミドルウェアを呼び出す
        await _next(context);

        // 次を呼び出した後の処理
        await context.Response.WriteAsync("Middleware 1 after\n");
    }
}

// 2つ目のミドルウェア
public class Middleware2 {
    private readonly RequestDelegate _next;

    public Middleware2(RequestDelegate next) {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context) {
        await context.Response.WriteAsync("\tMiddleware 2 before\n");

        await _next(context);

        await context.Response.WriteAsync("\tMiddleware 2 after\n");
    }
}

// 最後に呼び出されるミドルウェア
public class Middleware3 {
    // 使わないけど
    private readonly RequestDelegate _next;

    public Middleware3(RequestDelegate next) {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context) {
        await context.Response.WriteAsync("\t\tMiddleware 3\n");
    }
}

public class Startup {
    public void ConfigureServices(IServiceCollection services) {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        // 最初に呼び出されるミドルウェア
        app.UseMiddleware<Middleware1>();

        // 2つ目のミドルウェア
        app.UseMiddleware<Middleware2>();

        // 最後に呼び出されるミドルウェア
        app.UseMiddleware<Middleware3>();
    }
}

実行すると上記と同じレスポンスが返るようになります。

普段UseStaticFilesメソッドやUseMvcメソッドを使っていると思いますが、その中で実際にはミドルウェアはクラスとして用意されていて、UseMiddleware拡張メソッドを使っているようです。

勉強になりました。