ASP.NET Core MVC 公式チュートリアルから実開発への橋渡し

## はじめに
Webフレームワークを初めて使う時、公式チュートリアルを利用する方は多いかと思います。
しかし、公式チュートリアルの知識だけで実運用に耐えうる開発を行えるかというと、それは無理だと思います。
ASP.NET Core MVCもご多分に漏れずこのような感じでした。

そこで、公式チュートリアルと実開発の間を埋める「続チュートリアル」的なものが欲しいと思い、この記事を書いた次第です。

この記事ではASP.NET Core MVCを使って、認証、認可、ログ出力等、どんなweb開発するにしても必要になってくる土台部分を取り扱おうと思います。

この記事の立ち位置は下のような感じでしょうか。
1. 公式チュートリアル
1. **この記事**
1. 個々のweb開発で固有のビジネスロジックの実装
1. デプロイ
1. 運用

気力があれば 3、4、5 の部分についても記事に出来たらと思っています。

https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/first-mvc-app/start-mvc?view=aspnetcore-6.0&tabs=visual-studio


## 対象者
* ASP.NET Core MVCの公式チュートリアルに一通り目を通した方
* web開発の経験があまり無い方




## 目次
[開発環境](#-開発環境)
[プロジェクトの準備](#プロジェクトの準備)
[事前準備](#事前準備)
[認証機能の追加](#認証機能の追加)
[認可機能の追加](#認可機能の追加)
[ログ出力](#ログ出力)
[フィルター処理](#フィルター処理)
[本番運用時のエラー画面の設定](#本番運用時のエラー画面の設定)
[appsettings.jsonのカスタマイズ](#appsettingsjsonのカスタマイズ)


## 開発環境
* Windows10
* Microsoft Visual Studio Professional 2019 Version 16.7.3
* ASP.NET Core 3.1


## プロジェクトの準備
触れるプロジェクトを準備します。
何も考えずにVisual Studioで以下の手順でプロジェクトを新規作成します。
1. 「ファイル」⇒新規作成⇒プロジェクト
1. 「ASP.NET Core Web アプリケーション」を選択
1. プロジェクト名等を入力（サンプルソースはContinuedTutorialにしています。）
1. 「Web アプリケーション（モデル ビュー コントローラー）」を選択
1. 「作成」ボタン押下

Visual Studioでデバッグ実行して以下の画面が出ればOKです。
これをベースに改修していきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/7ad48064-1c6b-b212-a41d-d786a892a44f.png" width=50%>







## 事前準備
Visual Studio 2019ではVisual Studioでアプリの実行中にcshtmlを修正しても即時反映（ホットリロード）してくれず、一度停止させてから再実行しないといけません。
必須ではないですが、これでは開発効率が悪いので即時反映させるための対応を行います。
NuGetパッケージの Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation を利用します。
対応は[こちらの記事](https://deezus.net/aspnetcore-view/)を参考にすると良いです。
インストールするバージョンは環境に合わせて下さい。

Visual Studio 2022ではホットリロードがサポートされているようなので不要かもしれません。
https://forest.watch.impress.co.jp/docs/news/1364859.html













## 認証機能の追加

### ID/パスワードによるログイン認証の追加
下のようなログイン画面を作成し、ID/PWを入力して認証されるとHome画面へリダイレクトされるようにします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/46e0f52d-94ea-f28a-e928-11bf1bbfe169.png" width=50%>
↓
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/7ad48064-1c6b-b212-a41d-d786a892a44f.png" width=50%>

* ビューの作成
ID/パスワードを入力、送信する画面です。
Views/Accountフォルダを追加して Login.cshtml を新規作成します。
```html:Views/Account/Login.cshtml
﻿@{
    ViewData["Title"] = "Login";

}
<h2>ログイン</h2>
<hr />
<div class="row">
    <div class="col-md-6">
        <form asp-controller="Account" asp-action="Login" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label for="loginId">ログインID</label>
                <input type="text" name="loginId" placeholder="ログインID" id="loginId" class="form-control" />
            </div>
            <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" name="password" placeholder="パスワード" id="password" class="form-control" />
            </div>
            <div class="form-group">
                <input type="submit" value="ログイン" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>


@section Scripts {
     @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```













* コントローラーの作成
    1. Controllers/AccountController.cs を「MVCコントローラー - 空」で新規作成します。
    1. 新規作成したコントローラにログイン認証用のアクションメソッド Login を作成します。

今回、ID/PWのチェックは本題ではないのでID/PWに何か入力されていれば認証が通るようにしてあります。
ログイン認証されるとHome画面へリダイレクトされます。
```csharp:Controllers/AccountController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace ContinuedTutorial.Controllers
{
    public class AccountController : Controller
    {
        //ログイン画面表示用
        [HttpGet]
        public IActionResult Login()
        {
            return View();
        }

        //ログイン画面からのPOST処理用
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Login(string loginId, string password)
        {
            // ID,PWに値が入っていれば認証を通す
            if (string.IsNullOrWhiteSpace(loginId) || string.IsNullOrWhiteSpace(password))
            {
                ModelState.AddModelError(string.Empty, "ログイン情報に誤りがあります。");
                return View("Index");
            }

            // 認証されたらHomeページへリダイレクトする
            return RedirectToAction(nameof(HomeController.Index), "Home");
        }
    }
}

```

* トップ画面ををログイン画面に変更する

「https\://localhost:XXXXXX/」でアクセスした際に表示される画面をログイン画面に変更します。
Startup.csのConfigureメソッド内の pattern: "{controller=Home}/{action=Index}/{id?}"); の「Home」を「Account」へ変更します。

```csharp:Startup.cs
   pattern: "{controller=Account}/{action=Login}/{id?}");// Home を Accountへ変更
```



### Cookie認証の導入
ID/パスワードでログイン認証した後はCookieに認証情報を保持させて、いちいちID/パスワードを入力せずに認証を行うようにします。
これによって、ログインしていない状態でログイン必須ページへアクセスしたときにログインページへリダイレクトさせる等の処理を実現できたりします。


* Cookie認証スキームの追加
Startup.csのConfigureServicesメソッドへ以下の2つの処理を追加します。

```csharp:Startup.cs
//・・・省略
       public void ConfigureServices(IServiceCollection services)
        {
            //追加 Cookie認証サービスを追加
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) // 既定の認証スキームを CookieAuthenticationDefaults.AuthenticationScheme にする
                .AddCookie(options => {
                    options.LoginPath = "/Account/Login"; //未認証ユーザーがアクセスした際のリダイレクト先を指定
                });

            //追加 全てのユーザーが認証必須とするようにフォールバックポリシーを設定する
            services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
            });
//・・・省略
```
services.AddAuthentication~ でCookie認証スキームを既定の認証スキームとして設定しています。
また、未認証ユーザがアクセスした際のリダイレクト先をログイン画面（/Account/Login）としています。
services.AddAuthorization~ で全ユーザに対して認証を必須にしています。

参考URL
[ASP.NET Core の認証の概要](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/?view=aspnetcore-6.0)
[認可によって保護されたユーザー データを使って ASP.NET Core Web アプリを作成する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authorization/secure-data?view=aspnetcore-6.0#require-authenticated-users)

* 認証ミドルウェアの追加
認証ミドルウェアを追加することで、先ほどのCookie認証機能が利用できるようになります。
Startup.csのConfigureメソッドに以下ように1行追加します。

```csharp:Startup.cs
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //・・・省略

            app.UseRouting();

            app.UseAuthentication(); // 追加　認証ミドルウェアの追加

            app.UseAuthorization();

            //・・・省略
        }
    }
```
参考URL
[ASP.NET Core の認証の概要](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/?view=aspnetcore-6.0)
[ASP.NET Core Identity を使用せずに cookie 認証を使用する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/cookie?view=aspnetcore-6.0)


* ログインページは認証無しでアクセス可能にする
今のままではログインページも認証済みのユーザしか表示できない状態です。
ログインページは未認証で表示できるように AccountControllerに [AllowAnonymous] を追加します。
```csharp:AccountController.cs
//・・・省略
    [AllowAnonymous] //追加　認証なしでアクセス可能にする
    [TypeFilter(typeof(MyExceptionFilter))]
    public class AccountController : Controller
    {
//・・・省略
```

* 認証cookieを作成する
認証Cookieをレスポンスに追加する処理を AccountController の Loginメソッドに追加します。
★の箇所が追加、修正箇所です。

```csharp:AccountController.cs
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(string loginId, string password)// ★修正　非同期メソッドへ変更
        {
            // ID,PWに値が入っていれば認証を通す
            if (string.IsNullOrWhiteSpace(loginId) || string.IsNullOrWhiteSpace(password))
            {
                ModelState.AddModelError(string.Empty, "ログイン情報に誤りがあります。");
                return View("Index");
            }

            // ★追加　ClaimsPrincipalを構築
            var claims = new[] {
                new Claim(ClaimTypes.Name, loginId), // ユーザID
            };
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var principal = new ClaimsPrincipal(identity);

            // ★追加　暗号化された認証cookieをレスポンスに追加する
            await HttpContext.SignInAsync(principal);

            // 認証されたらHomeページへリダイレクトする
            return RedirectToAction(nameof(HomeController.Index), "Home");
        }
```
参考URL
[ASP.NET Core Identity を使用せずに cookie 認証を使用する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/cookie?view=aspnetcore-6.0)

* ログアウト処理の追加
現在のユーザーをサインアウトさせてその cookie を削除するには、SignOutAsync を呼び出します。
AccountController にログアウト用のLogoutメソッドを追加します。

```csharp:AccountController.cs
        public async Task<IActionResult> Logout()
        {
            // 認証Cookieをレスポンスから削除
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            // ログイン画面にリダイレクト
            return RedirectToAction(nameof(Login));
        }
```

* ログアウトリンクの作成
Views/Shared/_Layout.cshtml へログアウトリンクを作成します。
```html:C:Views/Shared/_Layout.cshtml
・・・省略
        <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
            <ul class="navbar-nav flex-grow-1">
                <li class="nav-item">
                    <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                </li>
                @*追加　ここから*@
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Logout">Logout</a>
                        </li>
                @*追加　ここまで*@
            </ul>
        </div>
・・・省略
```

<br>

以上で、ログインしていない状態でログインページ以外へアクセスするとログインページへリダイレクトされるようになっています。
試しにログインしていない状態で「http\://localhost:XXXXX/Home」等へアクセスしてみてください。

## 認可機能の追加
認可とは、ある機能や画面は管理者しか利用できないようする等の制御の事です。
今回はロールベースでの認可を導入し、管理者のみがアクセス可能なページを作成しようと思います。

### 今からやること
管理者ユーザ（ロール）でログインした場合のみ、Home画面に管理者用ページへのリンクを表示
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/2046f2bc-397e-967c-207c-cd8a154677e9.jpeg" width=50%>
管理者（ロール）が管理者用ページへのURLにアクセスすると管理者ページが表示される
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/2488d5c7-882d-89bd-9860-4badfcadba91.jpeg" width=50%>

一般ユーザが管理者用ページのURLにアクセスしたとしても拒否するようにする
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/9b22ce08-a351-856b-1ac1-a95892012549.jpeg" width=50%>




### 管理者ユーザ（ロール）でログインした場合のみ、Home画面に管理者用ページへのリンクを表示させる
* 管理者ページ用のビューを作成
Views/Adminフォルダを追加して、その中に Index.cshtml を新規作成します。
```html:Views/Admin/Index.cshtml
@{ ViewData["Title"] = "Admin"; }
<h1>管理者ページ</h1>
```

* 管理者用コントローラをを追加
Controllers/AdminController.cs を「MVCコントローラー - 空」で新規作成します。

```csharp:Controllers/AdminController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace ContinuedTutorial.Controllers
{
    public class AdminController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

```

* ログイン時にCookieにロール情報を保持させる
ログイン時に認証Cookieにロール情報を保持するように修正します。
AccountController内の以下の★の箇所の様に追加・修正を行います。

```csharp:AccountController.cs
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginAsync(string loginId, string password)
{
    if (string.IsNullOrWhiteSpace(loginId) || string.IsNullOrWhiteSpace(password))
    {
        ModelState.AddModelError(string.Empty, "ログイン情報に誤りがあります。");
        return View("Index");
    }

	// ★暫定的にAdministratorとGeneralという2つのロールを利用している
	// 通常はDB等に保存してあるロール情報を取得してくる
	string role = loginId == "admin" ? "Administrator" : "General"; //★　追加

	// ClaimsPrincipalを構築
	var claims = new[] {
	    new Claim(ClaimTypes.Name, loginId), // ユーザID
	    new Claim(ClaimTypes.Role, role) // ★追加ロール
	};
	var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
	var principal = new ClaimsPrincipal(identity);

	// 暗号化された認証cookieをレスポンスに追加する
	await HttpContext.SignInAsync(principal);

	// 認証されたらHomeページへリダイレクトする
	return RedirectToAction(nameof(HomeController.Index), "Home");
}
```

* Home画面に管理者用ページへのリンクを作成する
ログインユーザのロールが「Administrator」の時のみ、Home画面に管理者ページへのリンクが表示されるようにします。
Views/Home/Index.cshtmlに以下の★の範囲を追加します。

```html:Views/Home/Index.cshtml
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

@*★★★★★★ここから★★★★★*@
@if (Context.User.IsInRole("Administrator"))//ログインユーザのロールが「Administrator」か判定
{
    <div class="text-center">
        <a asp-controller="Admin" asp-action="Index">管理者用ページへのリンク</a>
    </div>
}
@*★★★★ここまで★★★★★★*@
```

ここまで終われば管理者でログインすると管理者用ページへのリンクが表示され、一般ユーザだと表示されていない状態になっていると思います。


### 管理者用ページには管理者（ロール）しかアクセス出来ないようにする
実は今のままでは一般ユーザでもログイン状態で管理者用ページのURL「https\://localhost:XXXXX/Admin/Index」を知っていれば、直接ブラウザにURLを入力することで管理者ページへアクセス出来てしまいます。

これに対処していきます。

* コントローラに認可設定を行う
AdminController.cs に Authorize属性 を付加してロールが「"Administrator"」以外はアクセス出来ないようにします。

```csharp:AdminController.cs
    [Authorize(Roles = "Administrator")] //追加　ロールが"Administrator"の場合のみアクセス可能
    public class AdminController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
```
この状態で一般ユーザが管理者用ページのURL「https\://localhost:XXXXX/Admin/Index」にアクセスすると以下の画面になりアクセスが出来なくなります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/9dbc10f5-8de8-2777-8991-2f7000c4a8df.jpeg" width=50%>

### アクセス拒否ページのカスタマイズ
アクセス拒否ページを以下の様な自作画面にしたいとおもいます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/9b22ce08-a351-856b-1ac1-a95892012549.jpeg" width=50%>

* アクセス拒否画面用のビューを作成
Views\Account\AccessDenied.cshtml　を新規作成します。
```html:Views\Account\AccessDenied.cshtml
<h2>アクセスが拒否されました</h2>
```

* アクセス拒否画面を返すアクションメソッドを追加
アセス拒否画面を表示するアクションメソッドを AccountController.cs に追加します。
```csharp:AccountController.cs
        public IActionResult AccessDenied()
        {
            return View();
        }
```

* アクセス拒否時のリダイレクト先を設定
Startup.cs 内のConfigureServicesメソッドでアセス拒否された際のリダイレクト先を作成したアクセス拒否画面に設定します。
```csharp:Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            //Cookie認証サービスを追加
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options => {
                    options.LoginPath = "/Account/Login"; 
                    options.AccessDeniedPath = "/Account/AccessDenied";//★追加 認可されていないリソースへのアクセス拒否された際のリダイレクト先
                });
```


ここまでで、一般ユーザでログインした状態で「https\://localhost:XXXXX/Admin/Index」へアクセスすると自作したアクセス拒否画面が表示されるはずです。








### 付録：コントローラー側でのロール情報の利用方法
コントローラー内で認証情報を取得する例が無かったので、ここで紹介したいと思います。

最終的には以下のイメージの様にHome画面にログインIDとロールを表示したいともいます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/0fd6e2b8-0d3f-66e4-12f6-29639bdf06bc.jpeg" width=50%>

* HomeController内で認証情報を取得

```csharp:HomeController.cs
    public class HomeController : Controller
    {
        //・・・省略

        public IActionResult Index()
        {
            string userId = User.FindFirst(ClaimTypes.Name).Value;//追加
            string role = User.FindFirst(ClaimTypes.Role).Value;//追加
            ViewData["UserID"] = userId;//追加
            ViewData["Role"] = role;//追加

            return View();
        }

        //・・・省略
```

* ビューで認証情報を表示
```html:Views/Home/Index.cshtml
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

@*★★★★ここから★★★★★★*@
@{
    string accessUser = $"ユーザー：{ViewData["UserID"]}　ロール：{ViewData["Role"]}　でアクセスしています。";
    <div class="text-center">
        <p>@accessUser</p>
    </div>
}
@*★★★★ここまでを追加★★★★★★*@
            :
```














## ログ出力
残念ながらAPS.NET Coreにはログをファイルに出力する機能がありません。
その為、ログをファイル出力するにはサードパーティ製のライブラリを利用する必要が有ります。
今回はNuGetパッケージのNLogを利用して、いわゆるアプリケーションログをファイル出力してみます。

### NLogの導入
[こちら](https://sorceryforce.net/ja/tips/asp-net-core-log-n-log)を参考にNLogを導入し、ファイルへのログ出力が出来るようにします。
今はファイルへログ出力だけ確認出来ればOKとして先に進みたいので、「ログをアーカイブする」以降の項目は今は扱わないです。
これ以降はログをファイル出力出来ている前提で進みます。

参考URL
[組み込みのログ プロバイダー](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/logging/?view=aspnetcore-6.0#built-in-logging-providers)
[サードパーティ製のログ プロバイダー](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/logging/?view=aspnetcore-6.0#third-party-logging-providers)
[NLog を使用してログを出力する](https://sorceryforce.net/ja/tips/asp-net-core-log-n-log)






## フィルター処理
例えば、全てのアクションメソッドの開始と終了時にログを出したい時、全てのコントローラー内に処理を書くのは面倒です。
そんな時にフィルターを使うと指定したコントローラーに対して決まったタイミングでログ出力が出来て効率的です。

フィルターを利用してアクションメソッドの実行前後にログ処理を挿入した場合のフロー概要は以下の通りです。
```
フィルター処理（アクションメソッド実行前）
　　　↓
アクションメソッド実行
　　　↓
フィルター処理（アクションメソッド実行後）
```
フィルターには様々な種類があるので[公式ページ](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0#exception-filters)を確認すると良いかと思います。

以下では「アクセスログの出力」、「予期せぬ例外の補足」をフィルターを利用して実装しています。

### アクセスログの出力
ここではユーザーがページへアクセスした際のアクセスログをフィルターを利用して出力してみます。

* ログ出力用のフィルターを作成
Filtersフォルダを作成してその中にAccessLogFilter.csを新規作成します。
アクションメソッド実行前に行いたい処理を OnActionExecuting、実行後に行いたい処理を OnActionExecuted に記述します。
例ではアクションフィルターを利用して、ログインID、コントローラー名、アクション名をログ出力しています。
```csharp:Filters/AccessLogFilter.cs
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog.Web;
using System;
using System.Security.Claims;


namespace ContinuedTutorial.Filters
{
    public class AccessLogFilter : IActionFilter
    {
        /// <summary>
        /// アクションメソッド実行前の処理
        /// </summary>
        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            OutputAccessLog(filterContext, "Start");
        }

        /// <summary>
        /// アクションメソッド実行後の処理
        /// </summary>
        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            OutputAccessLog(filterContext, "End");
        }

        /// <summary>
        /// アクセスログを出力する
        /// </summary>
        private void OutputAccessLog(FilterContext filterContext, string starOrtEnd)
        {
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try
            {
                var controllerActionDescriptor = filterContext.ActionDescriptor as ControllerActionDescriptor;
                var name = filterContext.HttpContext.User.FindFirst(ClaimTypes.Name).Value;

                logger.Info($"Controller:{controllerActionDescriptor.ControllerName} Action:{controllerActionDescriptor.ActionName} User:{(name ?? "Not User")} {starOrtEnd}");
            }
            catch (Exception ex)
            {
                logger.Error("\r\n" + "ログ出力時にエラーが発生しました。" + ex);
            }
        }
    }
}

```

* コントローラーへフィルターを適用
フィルター処理を行いたいコントローラーにTypeFilter属性を付加します。
今回はHomeControllerにつけてみようと思います。
```csharp:Filters/HomeController.cs
    //・・・省略
    
    [TypeFilter(typeof(AccessLogFilter))] //追加
    public class HomeController : Controller
    {
    
    //・・・省略
```

* HomeControllerで適当にログを出してみる
HomeControllerのIndexメソッドでログ出力してみます。
```csharp:Filters/HomeController.cs
//・・・省略
namespace ContinuedTutorial.Controllers
{
    [TypeFilter(typeof(AccessLogFilter))]
    public class HomeController : Controller
    {

    //・・・省略

        public IActionResult Index()
        {
            _logger.LogInformation("ログテスト");//★追加

            //・・・省略
```

この状態でログインしてHome画面を表示すると、以下の様なログがファイルへ出力されているはずです。
```
2022-04-19 22:07:11.3462 [0][Info ] Controller:Home Action:Index User:admin Start  (AccessLogFilter.OutputAccessLog(AccessLogFilter.cs:39))
2022-04-19 22:07:11.3968 [0][Info ] ログテスト  (HomeController.Index(HomeController.cs:26))
2022-04-19 22:07:11.4511 [0][Info ] Controller:Home Action:Index User:admin End  (AccessLogFilter.OutputAccessLog(AccessLogFilter.cs:39))
```

* ログイン/ログアウトのログ出力
こちらはフィルター処理ではなく、コントローラー内にログ出力処理を追加したいと思います。
何故かというと、前述のフィルターではログインのアクションメソッド実行前にフィルター処理が行われますが、この時IDやロール情報がまだ保存されていないので、取得できない為です。
同様の理由でログアウト後にもIDやロール情報が取得できません。

イメージ
```
フィルター処理（アクションメソッド実行前）　★ここでID等は取得できない
　　　↓
ログインアクションメソッド
　　　↓
フィルター処理（アクションメソッド実行後）
```

以降ではログイン成功、ログアウト成功時にログ出力してみたいと思います。


AccountControllerを以下の★の箇所の通り修正します。

コンストラクタ周り
```csharp:AccountController.cs
namespace ContinuedTutorial.Controllers
{
    [AllowAnonymous]
    public class AccountController : Controller
    {
        private readonly ILogger _logger; //★追加

        //★追加　loggerを引数で受け取るコンストラクタを作成
        public AccountController(ILogger<AccountController> logger)
        {
            _logger = logger;
        }

       //・・・省略
```
<br>
Loginメソッドにログイン成功のログ出力処理を追加

```csharp:AccountController.cs
       [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(string loginId, string password)
        {
            //・・・省略

            // ★追加　ログ出力
            _logger.LogInformation($"Controller:{nameof(AccountController)} Action:{nameof(AccountController.Login)} User:{loginId} Success!");

            // 認証されたらHomeページへリダイレクトする
            return RedirectToAction(nameof(HomeController.Index), "Home");
        }

        //・・・省略
```

<br>
Logoutメソッドにログアウト成功のログ出力処理を追加

```csharp:AccountController.cs
        public async Task<IActionResult> Logout()
        {
            // ★追加　IDの取得
            var userId = HttpContext.User.FindFirst(ClaimTypes.Name)?.Value;

            // 認証Cookieをレスポンスから削除
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            // ★追加　ログ出力
            if (!(userId is null))
            {
                _logger.LogInformation($"Controller:{nameof(AccountController)} Action:{nameof(AccountController.Logout)} User:{userId} Success!");
            }
            
            // ログイン画面にリダイレクト
            return RedirectToAction(nameof(Login));
        }
```

### 予期せぬ例外の補足
予期せぬ例外が発生した際に例外フィルターを利用してログ出力してみます。

* フィルターの作成
Filters/MyExceptionFilter.cs を新規作成します。
例ではフィルター内で例外の内容をログ出力しています。
```csharp:MyExceptionFilter.cs
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog.Web;
using System;
using System.Security.Claims;

namespace ContinuedTutorial.Filters
{
    public class MyExceptionFilter : IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try
            {
                var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
                var name = context.HttpContext.User.FindFirst(ClaimTypes.Name)?.Value;

                logger.Error(
                    $"Controller:{controllerActionDescriptor.ControllerName} " +
                    $"Action:{controllerActionDescriptor.ActionName} " +
                    $"User:{(name ?? "No User")} " +
                    "予期せぬ例外が発生しました。" + Environment.NewLine +
                    "************************************************" + Environment.NewLine +
                    $"{context.Exception}" + Environment.NewLine +
                    "************************************************"
                    );
            }
            catch (Exception ex)
            {
                logger.Error("\r\n" + "ログ出力時にエラーが発生しました。" + ex);
            }
        }
    }
}

```

* コントローラにフィルターを適用する
フィルターを適用したいコントローラーにTypeFilter属性を追加します。
例ではAccountControllerにフィルターを適用しています。
```csharp:AccountController
//・・・省略

namespace ContinuedTutorial.Controllers
{
    [AllowAnonymous] 
    [TypeFilter(typeof(MyExceptionFilter))] // ★追加
    public class AccountController : Controller
    {
        private readonly ILogger _logger;
        
        //・・・省略
```
* 試しに例外を出してみる
例ではログインID、パスワードを送信した際に例外が発生するように、AccountController の Login メソッドで例外を throw しています。
この時にログ出力されているはずです。

```csharp:AccountController
        //・・・省略
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(string loginId, string password)
        {
            //追加
            throw new Exception("ログイン時の例外テスト");

            if (string.IsNullOrWhiteSpace(loginId) || string.IsNullOrWhiteSpace(password))
            {
                 //・・・省略
```

ログイン画面でログインボタンを押すと以下の開発者用例外画面が表示され、ログファイルにも例外の内容が出力されているはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/6607ceeb-cb01-8c4c-8727-2968a41b223e.jpeg" width=50%>

### 本番運用時のエラー画面の設定
今のままだと、実際の運用環境で例外が発生した際に一つ前の手順で表示した開発者用例外画面は表示されずに、ログイン画面が表示されるだけです。
というより開発者用例外画面は表示されない方が良いです。　[詳細はこちら](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/error-handling?view=aspnetcore-6.0#developer-exception-page)
（何故ログイン画面が表示されるかは以降の手順を進めていくと分かると思います。多分。）

そこで、本番運用時でも例外が発生したことが分かるようにエラー画面を設定してみたいと思います。
ここではプロジェクト作成時にデフォルトで用意されている以下の様なエラー画面を出してみようと思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/d72daf9a-7205-8779-3081-1817bc475e07.jpeg" width=50%>

* 一時的に環境変数ASPNETCORE_ENVIRONMENTを本番用に設定する
環境変数 ASPNETCORE_ENVIRONMENT を設定することで開発環境、本番環境を切り替えられます。
ASPNETCORE_ENVIRONMENT の詳細はこちら [ASP.NET Core で複数の環境を使用する](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/environments?view=aspnetcore-6.0)<br>
Properties/launchSettings.json 内の"IIS Express"の箇所の"ASPNETCORE_ENVIRONMENT"に"Production"を設定します。

```json:Properties/launchSettings.json
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:51753",
      "sslPort": 44308
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        //"ASPNETCORE_ENVIRONMENT": "Development" コメントアウト
        "ASPNETCORE_ENVIRONMENT": "Production" // ★修正
      }
    },
    "ContinuedTutorial": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

これでVisual Studioで「IIS Express」を選択して実行すると本番環境として実行してくれます。
![iis_express.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/a5734999-3ccb-e680-6f39-94e4ef6989f6.jpeg)

* エラー画面
プロジェクト作成時に自動で作成されている Views/Shared/Error.cshtml を利用します。

* エラー画面を表示する為のアクションメソッドの追加
AccountController内にErrorメソッドを追加します。
これはHomeControllerに同じものがあるので、そちらをコピペしてもOKです。
```csharp:AccountController.cs
//・・・省略

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
//・・・省略
```
これはErrorViewModelのインスタンスを生成して、ビュー Views/Shared/Error.cshtml で利用するという記述です。
Views/Account内にError.chtmlが無いのになぜError.cshtmlが利用できるのか疑問な方は [ビューの検出](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/views/overview?view=aspnetcore-6.0#view-discovery)
をご確認ください。
簡単に言うと利用するビューを探すパスの順番があるということです。

* エラー画面のURLの設定
Startup.cs内のConfigureメソッド内でURLを「/Home/Error」から「/Account/Error」に修正します。

```csharp:Startup.cs
//・・・省略
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Account/Error"); //★修正
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
//・・・省略
```

これで、ログイン画面でログインボタンを押下して例外が発生すると下のようなエラー画面が表示されるはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/d72daf9a-7205-8779-3081-1817bc475e07.jpeg" width=50%>

一番最初の段階で例外が発生してもエラー画面が表示されずにログイン画面に戻るだけだったのは、例外発生時に認証必須ページ（/Home/Error）へ飛ばそうとしたが、未認証状態でだったのでリダイレクト先のログイン画面が表示されたというわけです。多分。
ErrorメソッドをAccountControllerに追加したのはAccountControllerは[AllowAnonymous]が付いており、未認証でもアクセスできるためです。




## appsettings.jsonのカスタマイズ
設定ファイルであるappsettings.jsonに独自のパラメータを追加したい場合があります。
そんな時の実装方法の紹介です。

以下の手順を進める場合はAccountControllerに追加した例外のthrow処理を削除しておいてください。
```csharp:AccountController.cs
throw new Exception("ログイン時の例外テスト");
```

### 独自パラメータの追加
appsettings.jsonへMySettingsセクションを追加します。
```json:appsettings.json
{
  "Logging": {
    "LogLevel": {
      //"Default": "Information",
      "Default": "Trace",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "MySettings": {//★追加
    "IntParam": 10,
    "StringParam": "続チュートリアル"
  }
}

```

### appsettings.jsonのエンコードを変更
appsettins.jsonがUTF8でない場合、「"StringParam": "続チュートリアル"」のようにマルチバイト文字を入れると、この値をログ出力したり画面へ表示する際に文字化けする場合があります。
その為、以下の手順でappsettings.jsonをUTF8で保存しなおします。
1. Visual Studioでappsettings.jsonを開く
1. [ファイル]⇒[名前を付けてappsettings.jsonを保存]
1. 開いたダイアログの[上書き保存]ボタンの右側の三角ボタン⇒[エンコード付きで保存]
1. 開いたダイアログの「エンコード」を「Unicode (UTF-8 シグネチャ付き)」にして「OK」

### バインドするクラスを作成
追加したMySettinsをバインドするクラスを作成します。
Models/AppSettings.csを新規作成します。
以下の手順を踏むとソースを自動生成してくれるので楽です。

1. Models/AppSettings.csを新規作成
1. appsettings.json の中身をコピー
1. Visual Studioの「編集」
1. 「形式を選択して貼り付け」
1. 「JSONをクラスとして張り付ける」
1. 不要部分を削除

下の例ではappsetting.json内のバインドするセクション名を定数（暗黙的にstatic）としてSectionに設定しています。
```csharp:Models/AppSettings.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContinuedTutorial.Models
{
    public class AppSettings
    {
        /// <summary>
        /// appsetting.json内のバインドするセクション
        /// </summary>
        public const string Section = "MySettings";

        public int IntParam { get; set; }
        public string StringParam { get; set; }
    }
}
```

### Startup.csの修正
appsettings.jsonへ独自に追加したセクションとバインドクラスの紐づけ処理をConfigureServicesメソッドへ追加します。
```csharp:Startup.cs
//・・・省略
        public void ConfigureServices(IServiceCollection services)
        {
            //追加　appsettings.json内のAppSettings.Sectionで指定するセクションをクラスAppSettingsにバインドする
            services.Configure<AppSettings>(Configuration.GetSection(AppSettings.Section));

//・・・省略
```

### 独自パラメータの利用例
独自追加したパラメータをHome画面に出力してみます。

* コントローラーの修正
HomeController.csを以下の「★追加」の様に修正します。
```csharp:HomeController.cs
//・・・省略
    [TypeFilter(typeof(AccessLogFilter))]
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly AppSettings _settings; //★追加
        
        //★修正　引数に IOptions<AppSettings> settings　を追加
        public HomeController(ILogger<HomeController> logger, IOptions<AppSettings> settings)
        {
            _logger = logger;
            _settings = settings.Value;//★追加
        }

        public IActionResult Index()
        {
            _logger.LogInformation("ログテスト");
            string userId = User.FindFirst(ClaimTypes.Name).Value;
            string role = User.FindFirst(ClaimTypes.Role).Value;
            ViewData["UserID"] = userId;
            ViewData["Role"] = role;
            ViewData["IntParam"] = _settings.IntParam; //★追加　整数パラメータ
            ViewData["StringParam"] = _settings.StringParam; //★追加　文字列パラメータ

            return View();
        }

//・・・省略
```

* ビューの修正
Views/Home/Index.cshtmlを修正します。
修正箇所は以下の「//★追加」の箇所です。

```html:Views/Home/Index.cshtml
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

@*★★★★ここから★★★★★★*@
@{
    string param = $"整数パラメータ：{ViewData["IntParam"]}　文字列パラメータ：{ViewData["StringParam"]}";
    <div class="text-center">
        <p>@param</p>
    </div>
}
@*★★★★ここまでを追加★★★★★★*@


            ・・・省略
```

<br>
ログインしてHome画面を確認すると、appsettings.jsonに設定したパラメータが表示されているはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1079274/d24658cb-8f1f-fd08-c446-1a7712abbd02.jpeg" width=50%>

<br>
<br>

今回の様に[クラスにバインドしない方法](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1)もありますが、クラスでバインドすると整数パラメータに文字列を設定してしまったような場合に例外を吐いて教えてくれるのでこちらにしました。


参考URL
[オプションパターンを使用して、階層型の構成データをバインドします](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#bind-hierarchical-configuration-data-using-the-options-pattern-1)

## おわりに
これを組み込んだ方が良い、もっと良い方法がある、内容が間違っている等がありましたら教えて下さい。


## 参考にしたサイト
### 公式
[ASP.NET Core MVC 公式チュートリアル](https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/first-mvc-app/start-mvc?view=aspnetcore-6.0&tabs=visual-studio)
[ASP.NET Core フィルター](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0)
[ASP.NET Core の認証の概要](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/?view=aspnetcore-6.0)
[ASP.NET Core で特定のスキームを使用して認可する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0)
[認可によって保護されたユーザー データを使って ASP.NET Core Web アプリを作成する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authorization/secure-data?view=aspnetcore-6.0#require-authenticated-users)
[ASP.NET Core Identity を使用せずに cookie 認証を使用する](https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/cookie?view=aspnetcore-6.0)
[組み込みのログ プロバイダー](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/logging/?view=aspnetcore-6.0#built-in-logging-providers)
[サードパーティ製のログ プロバイダー](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/logging/?view=aspnetcore-6.0#third-party-logging-providers)
[ASP.NET Core のエラーを処理する](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/error-handling?view=aspnetcore-6.0#developer-exception-page)
[ASP.NET Core で複数の環境を使用する](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/environments?view=aspnetcore-6.0)
[ビューの検出](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/views/overview?view=aspnetcore-6.0#view-discovery)
[ASP.NET Core の構成](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0)
[オプションパターンを使用して、階層型の構成データをバインドします](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#bind-hierarchical-configuration-data-using-the-options-pattern-1)

### 公式以外
[ASP.NETCore3.0でViewの変更をリアルタイムに反映させる](https://deezus.net/aspnetcore-view/)
[Cookie 認証 を使用してログインしないとリダイレクトされる仕組みを作る](https://sorceryforce.net/ja/tips/asp-net-core-cookie-authentication)
[へっぽこプログラマーの備忘録](https://kuttsun.blogspot.com/2017/09/aspnet-core-identity.html)
[NLog を使用してログを出力する](https://sorceryforce.net/ja/tips/asp-net-core-log-n-log)