先日書いた問題の原因が分かった(たぶん)

プロジェクトのプロパティをいじったら未入力チェックがかからなくなった

Visual Studio 2022でやってたんだけど、プロジェクトのプロパティで「ビルド」→「全般」にある「Null許容」が「有効化」になってると先日書いたような挙動をするようだ。

f:id:yagiey:20211204141827p:plain
プロジェクトのプロパティ

プロジェクトファイルとしては、Nullable要素がそれにあたるらしい。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Data.SqlClient" Version="4.8.3" />
  </ItemGroup>

</Project>

「<Nullable>enable</Nullable>」の行を削除して実行すると、以下のようになった。

f:id:yagiey:20211204144012p:plain
値型のプロパティだけに必須チェックがかかった

自分が知ってるNull許容型といえば、int?とかの Nullable<T> where T : struct なジェネリッククラスだけど、なぜその設定が今回の問題に関係あるのか意味が分からない。
でも勉強の糸口を見つけたので大きな前進!

VS2019とVS2022での生成されるプロジェクトの違い

僕のPCには、VS2019とVS2022が入ってて、2022ではMVCのプロジェクトを新規作成したときには以下のようなプロジェクトファイルが生成される。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

一方で、2019で同じくMVCのプロジェクトを新規作成したときは

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

というプロジェクトファイルが生成される。んで、これに対応するプロジェクトのプロパティ画面は以下。

f:id:yagiey:20211204143227p:plain
プロジェクトのプロパティ(VS2019)

モデルの未入力チェックの挙動が理解できない

なんで勝手に未入力チェックされるのん?

以下のコードで、新規Personを追加しようとするきの話し。

public class PersonController : Controller
{
    public IActionResult Create()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create(Person p)
    {
        if (!ModelState.IsValid)
        {
            return View();
        }

        DbConnection conn = _dbConn.GetConnection();

        try
        {
            PersonDao dao = new PersonDao(conn);
            long id = dao.InsertQuery(p);
        }
        catch (Exception)
        {
            throw;
        }
        finally
        {
            _dbConn.Close(conn);
        }

        return RedirectToAction(nameof(Index));
    }
}

public class Person
{
    [Display(Name = "ID")]
    public long PersonID { get; set; }
    [Display(Name = "名")]
    public string GivenName { get; set; }
    [Display(Name = "姓")]
    public string FamilyName { get; set; }
    [Display(Name = "生年月日")]
    public DateTime BirthDay { get; set; }
    [Display(Name = "性別")]
    public int Sex { get; set; }
    [Display(Name = "メールアドレス")]
    public string EmailAddress { get; set; }
}
@model Person
@{
    ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Person</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="FamilyName" class="control-label"></label>
                <input asp-for="FamilyName" class="form-control" />
                <span asp-validation-for="FamilyName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="GivenName" class="control-label"></label>
                <input asp-for="GivenName" class="form-control" />
                <span asp-validation-for="GivenName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="BirthDay" class="control-label"></label>
                <input asp-for="BirthDay" class="form-control" />
                <span asp-validation-for="BirthDay" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Sex" class="control-label"></label>
                <input asp-for="Sex" class="form-control" />
                <span asp-validation-for="Sex" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="EmailAddress" class="control-label"></label>
                <input asp-for="EmailAddress" class="form-control" />
                <span asp-validation-for="EmailAddress" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

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

どれも未入力のままCreateボタンを押下すると、どのプロパティにも検証のための属性つけてないのに、すべての項目で必須チェックがかかった。なんでだろう...。(生年月日に時刻まで含まれている点や性別が選択方式ではない点はとりあえず無視してください。)

f:id:yagiey:20211202133731p:plain
全項目に必須チェックかかった

HTMLを覗いてみると、以下のようになってた。

<h4>Person</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form action="/Person/Create" method="post">
            
            <div class="form-group">
                <label class="control-label" for="FamilyName"></label>
                <input class="form-control" type="text" data-val="true" data-val-required="The 姓 field is required." id="FamilyName" name="FamilyName" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="FamilyName" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <label class="control-label" for="GivenName"></label>
                <input class="form-control" type="text" data-val="true" data-val-required="The 名 field is required." id="GivenName" name="GivenName" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="GivenName" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <label class="control-label" for="BirthDay">生年月日</label>
                <input class="form-control" type="datetime-local" data-val="true" data-val-required="The 生年月日 field is required." id="BirthDay" name="BirthDay" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="BirthDay" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <label class="control-label" for="Sex">性別</label>
                <input class="form-control" type="number" data-val="true" data-val-required="The 性別 field is required." id="Sex" name="Sex" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Sex" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <label class="control-label" for="EmailAddress">メールアドレス</label>
                <input class="form-control" type="text" data-val="true" data-val-required="The メールアドレス field is required." id="EmailAddress" name="EmailAddress" value="" />
                <span class="text-danger field-validation-valid" data-valmsg-for="EmailAddress" data-valmsg-replace="true"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
            <input name="__RequestVerificationToken" type="hidden" value="【ほげほげ】" />
        </form>
    </div>
</div>

<div>
    <a href="/Person">Back to List</a>
</div>

値型のintやDateTimeも関係なく全項目value=""になってる。

コントローラーでモデルを作ってビューに渡してみた

問題とは関係ないけど、モデルをnewしてビューに渡してみた。

public IActionResult Create()
{
    Person p = new Person();
    p.FamilyName = "鈴木";
    p.Sex = 2;
    return View(model: p);
}

新規Personを追加しようとページにアクセスしてみると、指定した姓と性別がちゃんと表示された。

f:id:yagiey:20211202142545p:plain
指定した値が初期値として表示された

ほかの項目はvalue=""だった。string型のデフォルト値であるnullが設定された結果そうなってんだろうな。
ここからCreateボタンを押下すると、やはり未入力チェックがかかった。

f:id:yagiey:20211202143109p:plain
やっぱり未入力チェックがかかった

いやいや、どれもRequiredAttributeつけてないって。なんで勝手に必須チェックするん?

javascript切ってみた

今まで見てきたバリデーションは、クライアント側でやってるっぽいので、javascriptを無効にしてみた。
同様に、初期表示の状態でCreateボタンを押してみたら、POSTのCreateアクションを通って、やっぱり未入力のエラーメッセージが出た。

f:id:yagiey:20211202144009p:plain
サーバー側で未入力チェックがかかった

いやいや、だから、必須項目なんて一つも無いって!

いろいろ理解できない

  • なぜ勝手に未入力チェックがかかるのか?
  • 新規入力の画面のビューに対して、コントローラー側でモデルを作って渡してやるべきなのか?

なぜか分らんけど動いた

SchoolContext.csのメソッドOnModelCreatingのコメントの行を書き直したら動くようになった。先日のエラーログから、ここがテーブル名になるのかなって感じでやってみた。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Course>().ToTable("Course");
  modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
  modelBuilder.Entity<Student>().ToTable("Person"); // modified
  modelBuilder.Entity<Department>().ToTable("Department");
  modelBuilder.Entity<Instructor>().ToTable("Person"); // modified
  modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
  modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
  modelBuilder.Entity<Person>().ToTable("Person");

  modelBuilder.Entity<CourseAssignment>()
      .HasKey(c => new { c.CourseID, c.InstructorID });
}

こうすると、たとえば Student/Index にアクセスしたときに走るSQLは以下だそうだ。

SELECT
  [p].[ID]
 ,[p].[Discriminator]
 ,[p].[FirstName]
 ,[p].[LastName]
 ,[p].[EnrollmentDate]
FROM
  [Person] AS [p]
WHERE
  [p].[Discriminator] = N'Student'
ORDER BY
  [p].[LastName]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

なにがどうなってWHERE句がちゃんと追加されてるんだろう。Person、Student、InstractorどのエンティティにもDiscriminatorなんてプロパティ無いけどPersonテーブルにはカラムがあるぞ?Table-per-Hierarchy (TPH) 継承を実現する為に、こうなるようになってるのかな?

githubに何か書いてあったYO!!

github.com

エンティティを継承したい

ASP.NET Core MVCとEntity Framework Coreに関して、Microsoft公式のチュートリアルシリーズをちょこちょこやってる。
docs.microsoft.com
これの、「チュートリアル: 継承を実装する - ASP.NET MVC と EF Core」でつまづいている。
docs.microsoft.com

「実装をテストする」にて、「アプリを実行して、さまざまなページを試してください。 すべてが前と同じように動作します。」と記載があるが、実行してトップページのナビゲーションの「About」「Students」「Instructors」「Departments」から各ページへ遷移するとエラーになる。そりゃそうだ。StudentテーブルやInstractorテーブルがなくなったにもかかわらず、例えばStudentsControllerのIndexメソッドで

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));

ってやってるからじゃないかな?出力されたログには

Microsoft.Data.SqlClient.SqlException (0x80131904): Invalid object name 'Student'.

ってあるし。


チュートリアルに記載してある手順は全部踏んだつもりだけど動かない。あまりにも当たり前すぎて書いてないのかな?ってことは僕の理解力がうんこなのかな。
どう修正したらちゃんと動くのか考えてみよう。まだよく分らんけど、書き直すのは

  • SchoolContext.csのメソッドOnModelCreatingでやってるmodelBuilder.Entity().ToTable("Student")とか
  • 各コントローラーで使ってる_context.Studentsとか_context.Instructorsとか

らへんかな?

ユーザーシークレットの動きについてちょっと調べてみた

お久しぶりです。100億年ぶりにプログラミングを始めました。
先日、Visual Studio 2019をダウンロードして、何やらいろいろ調べながらやっています。
DBの接続文字列など、以前は config ファイルに書いていたように記憶していますが、何やらいろいろ仕組みが変わっていて、今は appsettings.json に書くのがよさそう。
appsettings.json に関連して、機密情報を扱うときは「ユーザーシークレット」なるものを使うと良さげなことを知ったのですが、ちょっと気になったことを確かめてみました。

appsettings.jsonとダブった項目はどうなるのか?

ユーザーシークレットが使われるみたいです。以下のように記載して確かめてみました。

/* appsettings.json */
{
  "Login": {
    "Email": "hoge@example.com",
    "Password": "hoge"
  }
}
/* secret.js */
{
  "Login": {
    "Email": "secret@example.com",
    "Password": "secret"
  }
}

secret.jsonが存在しない場合はどうなるのか?(git clone した場合など)

ちゃんとappsettings.jsonの値が取得されました。

ユーザーシークレットに関する設定はプロジェクトファイルに以下のように記述されるようです。
この長いIDと同じ名前のフォルダが C:\Users\【ユーザー名】\AppData\Roaming\Microsoft\UserSecrets に作られて、その中に secret.json が置かれるようです。この設定を残したまま、ユーザーシークレットのフォルダを消して確認してみました。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>7a0d510b-d5ad-477a-a103-0d4d117cb851</UserSecretsId>
  </PropertyGroup>

</Project>

DIってなんだっけ?

appsettings.json で設定した値を取得するために、HomeController クラスのコンストラクタの引数を追加したけど、なんで好き勝手にコンストラクタの引数増やして良いの?何が起きてるの?勉強しよう。

ターミナルで実行したときとスクリプトファイルを実行したときで結果が違う

次のようなitems.xmlを作って、

<?xml version="1.0" encoding="UTF-8"?>
<items>
  <item>foo</item>
  <item>bar
baz</item>
  <item>qux</item>
</items>

bash

$echo "cat /items/item[2]" | xmllint --shell items.xml

したときは

/ >  -------
<item>bar
baz</item>
/ >

ってなるけど、

#!/bin/sh
echo "cat /items/item[2]" | xmllint --shell items.xml

を保存したファイルを実行したとき

/ > ------- <item>bar baz</item> / >

ってなるのなんでや!連続したスペース類がスペース1個にされてる...。困るなあ。連続スペースはどうでもいいけど、改行は改行のまま欲しいんだけどな...。

解決した

ファイルに保存するスクリプト

#!/bin/sh
str=`echo "cat /items/item[2]" | xmllint --shell items.xml`
echo "${str}"

ってしたら改行と連続スペースが保持されたままになった。わーい。
あとは、これをsedかなんかで処理して中身だけ取り出したい。