ASP.NET Core MVCでアプリ作る際に参考にしたリンク集

### はじめに
C#、ASP.NET初心者です。
現場で.NET Frameworkを使っているため勉強として、ASP.NETでアプリ（単語帳）を作成しました。
その際に参考にした記事をまとめます。


### 使用技術・バージョン
- C# 10.0
- ASP.NET Core MVC（.NET6.0.10）
- EntityFrameworkCore 6.0.7
- PostgreSQL 15
- jQuery 3.5.1
- Chart.js 4.2.1
- Bootstrap 5.1.0

### 機能一覧
- ログイン認証
- 単語の登録・更新・削除・一覧表示・ページング
- 単語の学習
- 学習結果表示
- 学習履歴のグラフ表示・Excel出力・一括削除


### プロジェクト作成～DB構築
アプリ作成の前にチュートリアルも行い、今回もベースとして参考にしました。
[公式チュートリアル](https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/first-mvc-app/start-mvc?view=aspnetcore-6.0&tabs=visual-studio)

DBにPostgreSQLを選んだため以下の記事も参考にしました。
[PostgreSQL で Movie チュートリアル (CORE)](http://surferonwww.info/BlogEngine/post/2022/07/18/use-posrgresql-for-movie-tutorial-instead-of-sql-server.aspx)

なぜPostgreSQLにしたかというと、今までHerokuでデプロイするためにPostgreSQLを使用しており一番使い慣れていたからです。
（今回もHerokuでデプロイしようと思っていたのですが有料化のためやめました）
しかし、コードファーストでDB作成する場合、PostgreSQLでは不都合な点があり苦労しました。
詳細はこちら→[【EF Core】コードファーストでPostgreSQLのDBを作成する【ASP.NET】](https://qiita.com/1noseA/items/861b64549fe7083e4347)
特にこだわりがない人はSQL Serverでやるのがいいと思います。

### ログイン認証
今回認証機能はおまけのためさらっと参考記事のコピペで実装しました。
[【ASP.NET(C#)３】.NET 6でMVCのログイン認証を作成。Core 3.1との違いも解説](https://lightning-shine.com/post3091/)
（上記記事は以下をベースにしています。バージョンは古いのですがそのままでいけました）
[ASP.NET Core Identity を使わない認証](https://kuttsun.blogspot.com/2017/09/aspnet-core-identity.html)

また、ポートフォリオ用に入力しなくてもログインできる「テストログイン」機能も実装しました。
（テストログインボタン押下の場合、ControllerでViewModelにテスト用ユーザ名とパスワードを設定するようにしています）
```Account/Index.cshtml
<form asp-antiforgery="true" asp-controller="Account" asp-action="Login" class="needs-validation" novalidate>
    <div class="mb-3">
        <label for="userName" class="form-label">ユーザ名</label>
        <input type="text" name="UserName" id="userName" class="form-control box-shadow" required />
        <div class="invalid-feedback" style="display: block;">
            <span asp-validation-for="UserName"></span>
        </div>
    </div>
    <div class="mb-3">
        <label for="password" class="form-label">パスワード</label>
        <input type="password" name="Password" id="password" class="form-control box-shadow" required />
        <div class="invalid-feedback" style="display: block;">
            <span asp-validation-for="Password"></span>
        </div>
    </div>
    <div class="text-center mt-5">
        <input type="submit" value="ログイン" class="btnCommon btnGreen btnBig w-variable box-shadow" name="btnName" />
    </div>
    <div class="text-center mt-4">
        <input type="submit" value="テストログイン" class="btnCommon btnBlue btnBig w-variable box-shadow" name="btnName" />
    </div>
</form>
```

```AccountController.cs
[HttpPost]
[AutoValidateAntiforgeryToken]
public async Task<IActionResult> Login(AccountViewModel viewModel, string btnName)
{
    // テストログインボタン押下の場合ログインを許す
    if (btnName == "テストログイン")
    {
        viewModel.UserName = "user1";
        viewModel.Password = "password1";
    }
    // 入力エラーがあったら画面再表示して処理を中断する
    else if (!ModelState.IsValid)
    {
        return View("Index", viewModel);
    }

    // ユーザー名が一致するユーザーを抽出
    var lookupUser = _context.Users
                            .Where(u => u.UserName == viewModel.UserName)
                            .FirstOrDefault();
    if (lookupUser == null)
    {
        viewModel.ErrorMsg = "ユーザ名かパスワードが間違っています。";
        return View("Index", viewModel);
    }

    // パスワードの比較
    if (lookupUser?.Password != viewModel.Password)
    {
        viewModel.ErrorMsg = "ユーザ名かパスワードが間違っています。";
        return View("Index", viewModel);
    }

    // Cookies 認証スキームで新しい ClaimsIdentity を作成し、ユーザー名を追加します。
    var identity = new ClaimsIdentity("MyCookieAuthenticationScheme");
    identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, lookupUser.UserId.ToString()));
    identity.AddClaim(new Claim(ClaimTypes.Name, lookupUser.UserName));

    // クッキー認証スキームと、上の数行で作成されたIDから作成された新しい ClaimsPrincipal を渡します。
    await HttpContext.SignInAsync("MyCookieAuthenticationScheme", new ClaimsPrincipal(identity));

    return RedirectToAction(nameof(WordsController.Index), "Words");
}
```

ちなみに現場でもログイン画面を担当し、ASP.NET Identityは使っていないです。使う機会があれば公式で勉強したいです。

### 登録・更新・削除・一覧表示
ASP.NETは作成したModelからスキャフォールディングでCRUDのControllerとcshtmlが自動生成されます。
しかし、cshtmlはそれぞれ1画面ずつ生成されるため、1つのcshtml（一覧画面）に機能をまとめました。
それでも1から自分が作成するより楽です。
[【ASP.NET(C#)５】データ一覧画面と作成画面を一緒にしたい。複数のmodel（モデル）参照方法](https://lightning-shine.com/post3360/)

ただ現場ではSQLで書いているのでEntity Framework Coreの書き方に慣れなかったです。今回作成するのに必要な最低限しか見ていないですが、深く勉強してみたかったです。
[Entity Framework Core で色々な SQL を投げてみる 1](https://mslgt.hatenablog.com/entry/2019/02/25/055939#20190225_select_limit_get_3_records)

完成版は以下です。
![スクリーンショット (26_1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/1531cdab-0234-23f0-48e8-92c8c649c788.png)


### ページング
公式をそのままコピペしました。
[チュートリアル: 並べ替え、フィルター処理、ページングを追加する - ASP.NET MVC と EF Core](https://learn.microsoft.com/ja-jp/aspnet/core/data/ef-mvc/sort-filter-page?view=aspnetcore-7.0)

### 単語の学習と結果表示
単語の意味を出題して、単語を答えるという機能です。
セッションや一時保存について学びたかったため、この機能がある単語帳を作ることに決めました。
現場ではセッションを使っているのですが、すでに共通として用意されたものであるため設定等がよく分からず、今回はTempDataを使用しました。

他にもViewBag、ViewDataと似たようなものがあり違いが分からず以下を参考に見ました。
[ASP.NET MVC の ViewData、ViewBag および TempData](https://blogs.jp.infragistics.com/entry/2016/02/26/093000)
アクション間でデータを渡したいのでTempDataがいいのではないかと思いました。

最初にDBから10問取得し、それをTempDataに保存して一問ずつ出題と答え合わせをしています。
回答と答え合わせの結果も保持して、学習後には結果画面で表示するようにしています。

変数にTempDataの値を設定するにはキャストやデシリアライズ、TempDataに設定するにはシリアライズ、TempDataから値を取り出したら消えてしまうので「TempData.Keep();」が必要なのはめんどくさいと感じました。
例として答え合わせ処理のControllerを抜粋します。
```StudyController.cs
// 答え合わせ処理
// POST: Study/Answer
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Answer(StudyViewModel viewModel)
{
    // 省略

    // 保存データを取得
    viewModel.QuestionNo = (int)TempData["QuestionNo"];
    viewModel.CorrectAnswerCount = (int)TempData["CorrectAnswerCount"];
    viewModel.QuestionList = JsonConvert.DeserializeObject<List<Words>>((string)TempData["QuestionList"]);
    TempData.Keep();

    // 正答取得
    viewModel.CorrectAnswer = viewModel.QuestionList[0].Word;
    // 答え合わせする
    if (viewModel.MyAnswer == viewModel.CorrectAnswer)
    {
        viewModel.Judgement = "〇";
        // 正答数を加算する
        viewModel.CorrectAnswerCount++;
        TempData["CorrectAnswerCount"] = viewModel.CorrectAnswerCount;
    }
    else
    {
        viewModel.Judgement = "×";
    }
    // チェック済みにする
    viewModel.ChekedFlg = true;

    // 結果リストに格納する
    List<ResultViewModel> resultList = new List<ResultViewModel>();
    ResultViewModel result = new ResultViewModel();
    result.QuestionNo = viewModel.QuestionNo;
    result.Meaning = viewModel.QuestionList[0].Meaning;
    result.MyAnswer = viewModel.MyAnswer;
    result.CorrectAnswer = viewModel.CorrectAnswer;
    result.Judgement = viewModel.Judgement;
    // すでに結果リストがあったら追加する
    if (TempData["ResultList"] != null)
    {
        resultList = JsonConvert.DeserializeObject<List<ResultViewModel>>((string)TempData["ResultList"]);
        resultList.Add(result);
        TempData["ResultList"] = JsonConvert.SerializeObject(resultList);
    }
    else
    {
        resultList.Add(result);
        TempData["ResultList"] = JsonConvert.SerializeObject(resultList);
    }

    return View("Index", viewModel);
}
```

### グラフ表示
グラフはChart.jsを使用しており、以下の記事にまとめています。
[ASP.NET Core MVCでグラフを表示したい【Chart.js】](https://qiita.com/1noseA/items/fdc1dd76d173abfd0632)

### Excel出力
何かファイル操作をしてみたいと思い、ClosedXmlというライブラリを使用してExcel出力を実装しました。
ClosedXmlの説明は以下の記事が分かりやすいかもしれません。
[【C#】ClosedXml でExcel簡単操作（サンプル付き）](https://resanaplaza.com/%E3%80%90c%E3%80%91closedxml-%E3%81%A7excel%E7%B0%A1%E5%8D%98%E6%93%8D%E4%BD%9C%EF%BC%88%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E4%BB%98%E3%81%8D%EF%BC%89/)

これもいろいろな記事を見たのですがうまくいかず…以下の記事でできました。
[【ASP.NET MVC5】ClosedXMLでExcelファイル出力](https://ishii-singpg.com/archives/1355)
以下に抜粋します。
```HistoriesController.cs
// POST: Histories/DownloadFile
[HttpPost, ActionName("DownloadFile")]
[ValidateAntiForgeryToken]
public IActionResult DownloadFile()
{
    // ログインしていなければログイン画面へ
    if (_claim == null)
    {
        return RedirectToAction("Index", "Account");
    }

    // 出力対象の履歴を取得
    int userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
    var histories = _context.Histories
                            .Where(h => h.UserId == userId)
                            .ToList();
    // 出力項目編集
    DataTable dt = new DataTable("履歴");
    // ヘッダー行の内容設定
    dt.Columns.AddRange(new DataColumn[4] { new DataColumn("学習日"),
                                    new DataColumn("出題数(回)"),
                                    new DataColumn("正答数(回)"),
                                    new DataColumn("正答率(％)")});
    // 取得したデータをDataTableにコピー
    string rate = null;
    foreach (var h in histories)
    {
        rate = ((double)h.CorrectAnswerCount / (double)h.StudyCount * 100).ToString("F1");
        dt.Rows.Add(h.StudyDate, h.StudyCount, h.CorrectAnswerCount, rate);
    }
    // ワークブックを新規作成
    using (var wb = new XLWorkbook())
    {
        // ワークシートを追加する
        wb.Worksheets.Add(dt);

        // 出力
        using (MemoryStream stream = new MemoryStream())
        {
            // ワークブックを保存する
            wb.SaveAs(stream);
            return File(stream.ToArray(),
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                $"histories_{User.Identity.Name}_{DateTime.Now.ToString("yyyyMMdd")}.xlsx");
        }
    }
}
```

### デプロイ
Herokuが有料化のため他のものを探していました。
Fly.ioやAzureがいいのかなと思ったのですがまだ試してはいません。。
Fly.ioが無料でPostgreSQLでいけそうなのでやってみたいのですが、Dockerの知識がなく、Dockerの環境構築をしないとできないものなのか気になっています。。
[ASP.NET Core In Fly.io](https://github.com/Arshu/ASP.NET-Core-In-Fly.io)

### 最後に
今までにRails、Laravel、SpringBootなど使用したことありますが、それらに比べれば参考記事が少なく苦労しました。
それでもASP.NETの魅力や楽しさが分かり、今後も仕事で使うのであれば勉強をしていきたいと思えました。

もし詳しくコードを見たい場合は以下をご参照ください（拙いコードですが…）
https://github.com/1noseA/Flashcard