【C#入門】初学者がASP.NETでWebアプリを作る：第5回

#今回やること
アカウント認証の実装の続きです。

#前回足りてない修正
前回の状態で一度起動すると、エラーになってしまい起動できませんでした。
そこで下記2つの修正を行いました。

①Startup.csのConfigureServicesにAddRazorPagesを追加

```C#:Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();

            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
            services.AddControllersWithViews();
            services.AddRazorPages();　// 追加部分
        }
```

②_LoginPartial.cshtmlを修正

```html:_LoginPartial.cshtml
@using Microsoft.AspNetCore.Identity
@inject SignInManager<SMSUser> SignInManager　　←ここの型引数を独自ユーザクラスにする
@inject UserManager<SMSUser> UserManager　　　　←ここの型引数を独自ユーザクラスにする

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">ようこそ @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form  class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
            <button  type="submit" class="nav-link btn btn-link text-dark">ログアウト</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">アカウント登録</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">ログイン</a>
    </li>
}
</ul>

```

起動するとこのような画面になります。アカウント登録してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/3ae2706f-5991-d673-9759-a88ee9bf900a.png)

URLは変わったんですが何も画面遷移しませんでした。
対応するビューとかコントローラーがないからですね。
デフォルトだとASP.NET Core Identityのライブラリにそのあたりが含まれているようで、
独自クラスを使ったとたんそれらが使えなくなったってことですね…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/a57cb6c3-53a5-490b-19a0-d3c82e3dd3b7.png)

で、ライブラリに含まれているコントローラやらビューやらはスキャフォールディングで作れる機能があります。
プロジェクトを右クリックして、追加→新規スキャフォールディングアイテムでIDを選び、追加ボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/0e203061-27cc-f999-fd9c-44fea4b716e8.png)

自分に合った設定をします。オーバーライドはよくわからないので全部にしておきます。
（おそらくオーバーライドしたいもの（編集したい画面とか機能）をチェックすると、実体ができるのだと思いました。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/9ab2e857-694b-1964-368a-e1305ab4fd6c.png)

はいエラー。どうやらDbContextはIdentityDbContextを継承しないといけないようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/b7d7acdc-6204-397f-0fc4-82fb65df169a.png)

なので前回の修正を戻します。

```C#:ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace SalaryManagementSystem.Data
{
    public class ApplicationDbContext : IdentityDbContext
    //public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; }
        public DbSet<SalaryManagementSystem.Models.SMSUser> SMSUsers { get; set; }
    }
}
```

もう一度やってみます。ユーザークラスが選べねぇ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/2131470e-417c-4909-4790-90970ed5bb0b.png)

謎のテキストファイルが表示されました。うまくいったようです？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/5fb51f5e-2f94-92a0-7dfe-08d87335a9a3.png)

ユーザクラスが選べなかった（デフォルト：IdentityUserから変更できなかった）のは、DbContextの書き方がおかしかったようです。

```C#:ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SalaryManagementSystem.Models;
using System;
using System.Collections.Generic;
using System.Text;

namespace SalaryManagementSystem.Data
{
    public class ApplicationDbContext : IdentityDbContext <SMSUser>  //←ここに型引数が必要
    //public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<SalaryManagementSystem.Models.Salary> Salary { get; set; }
        public DbSet<SalaryManagementSystem.Models.SMSUser> SMSUsers { get; set; }
    }
}

```
もう一度スキャフォールディング…ユーザクラスは選べない。
追加を押すと、既にありますと言われますがOk。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/b10e2098-49ed-7c79-527d-318f4beeb2d3.png)

さらにStartup.csを修正します。

```C#:Startup.cs
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDatabaseDeveloperPageExceptionFilter();

            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders()
                .AddDefaultUI();        // ←追加
            services.AddControllersWithViews();
            services.AddRazorPages();
        }
```
登録画面には遷移するようになりました。ただ、入力項目等があるべき形になっていないので、修正する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/27067999-bc10-e7c1-abc9-ef72af8f7572.png)

#登録画面の修正
登録画面のロジックを修正していきます。
まずはコードビハインド。
（そもそもASP.NETはコードビハインドって言わない？WPFだけの話なのかな…）

```C#:Register.cshtml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using SalaryManagementSystem.Models;

namespace SalaryManagementSystem.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<SMSUser> _signInManager;
        private readonly UserManager<SMSUser> _userManager;
        private readonly ILogger<RegisterModel> _logger;
        //private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<SMSUser> userManager,
            SignInManager<SMSUser> signInManager,
            ILogger<RegisterModel> logger
            //IEmailSender emailSender)
            )
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
            //_emailSender = emailSender;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            //[Required]
            //[EmailAddress]
            //[Display(Name = "Email")]
            //public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} は {2} ～ {1} 文字で入力してください。", MinimumLength = 6)]
            [Display(Name = "ユーザID")]
            public string UserId { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} は {2} ～ {1} 文字で入力してください。", MinimumLength = 6)]
            [Display(Name = "ユーザ名")]
            public string UserName { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "{0} は {2} ～ {1} 文字で入力してください。", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "パスワード")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "パスワード再入力")]
            [Compare("Password", ErrorMessage = "パスワードが一致しません。")]
            public string ConfirmPassword { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                //var user = new SMSUser { UserName = Input.Email, Email = Input.Email };
                var user = new SMSUser { SMSUserId = Input.UserId, SMSUserName = Input.UserName, IsLocked = false, LoginFailCount = 0, CreateDate = DateTime.Now, UpdateDate = DateTime.Now };
                var result = await _userManager.CreateAsync(user, Input.Password);
                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    //code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    //var callbackUrl = Url.Page(
                    //    "/Account/ConfirmEmail",
                    //    pageHandler: null,
                    //    values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                    //    protocol: Request.Scheme);

                    //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    //    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    //if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    //{
                    //    return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    //}
                    //else
                    //{
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    //}
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }
    }
}
```
次にビュー。

```html:Register.cshtml
@page
@model RegisterModel
@{
    ViewData["Title"] = "アカウント登録";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <!--<h4>Create a new account.</h4>-->
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <!--<div class="form-group">
        <label asp-for="Input.Email"></label>
        <input asp-for="Input.Email" class="form-control" />
        <span asp-validation-for="Input.Email" class="text-danger"></span>
    </div>-->
            <div class="form-group">
                <label asp-for="Input.UserId"></label>
                <input asp-for="Input.UserId" class="form-control" />
                <span asp-validation-for="Input.UserId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.UserName"></label>
                <input asp-for="Input.UserName" class="form-control" />
                <span asp-validation-for="Input.UserName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <!--<h4>Use another service to register.</h4>
    <hr />
    @{
        if ((Model.ExternalLogins?.Count ?? 0) == 0)
        {
            <div>
                <p>
                    There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                    for details on setting up this ASP.NET application to support logging in via external services.
                </p>
            </div>
        }
        else
        {
            <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                <div>
                    <p>
                        @foreach (var provider in Model.ExternalLogins)
                        {
                            <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                        }
                    </p>
                </div>
            </form>
        }
    }-->
            <h4>各項目は必須項目です。</h4>
            <p>・ユーザIDはログイン時に使用します。6～10文字で入力してください。</p>
            <p>・ユーザ名は画面に表示されます。1～100文字で入力してください。</p>
            <p>・パスワードは6～100文字で入力してください。</p>
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

```

ほかにちょっとだけ直すところがあります。
_ManageNav.cshtml内で自作ユーザクラスを使っていますが、usingがないのでエラーになってしまいます。
なので、usingを追加しておきます。

```html:_ManageNav.cshtml
@using SalaryManagementSystem.Models;  ←追加
@inject SignInManager<SMSUser> SignInManager
```
下記のような画面になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/24affebb-e7c3-f307-8301-427817559415.png)

適当に入力してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/a1b774b2-54b8-8abc-38cb-f8843a5f5976.png)

Usernameが入ってないといわれました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/565a6801-c6a7-777a-fc94-732b20434165.png)

Register.cshtml.csをちょっと修正。不本意ながら使わないUserNameを設定。

```C#:Register.cshtml.cs
//var user = new SMSUser { UserName = Input.Email, Email = Input.Email };
var user = new SMSUser { SMSUserId = Input.UserId, SMSUserName = Input.UserName, UserName = Input.UserName, IsLocked = false, LoginFailCount = 0, CreateDate = DateTime.Now, UpdateDate = DateTime.Now };
```

どうやら日本語はだめらしい。[こちら](https://qiita.com/terandard/items/95aabef9de7b74793ba6)を参考に日本語対応する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/447e9dce-e457-29c6-494d-0d2db6f98160.png)

```C#:Startup.cs
            //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
            //    .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddIdentity<SMSUser, IdentityRole>(options =>
            {
                options.User.AllowedUserNameCharacters = null;
            })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders()
                .AddDefaultUI();
```

登録を押すとエラー。DBを更新せよと。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/fd720994-5973-671b-c7c7-f3f15a9fce5d.png)

とりあえず指示通りAdd-MigrationとUpdate-Databaseする。
Add-Migrationで警告が出るがまあなんでもいいので無視。

```
PM> Add-Migration Init_20210202
Build started...
Build succeeded.
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
To undo this action, use Remove-Migration.
PM> Update-Database
Build started...
Build succeeded.
Done.
```

登録できました。右上にユーザー名も表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/5ab5ea18-c57f-3ea2-b083-1f0fb5df4ef7.png)

DBを見ると、SMSUserテーブルではなく、AspNetUsersに登録されていました。ナンデ！？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/1452964e-80b4-b94d-c69b-47425c84c2bc.png)

ちなみに独自項目はAspNetUsersテーブルにできていました。UserName1ってなに…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57278/3db57b40-0d25-40ab-250d-b06fd51c4463.png)

#次回予告
次回はログインします。