コンテンツにスキップ

.NET

dotnet一般

.NET GitHub

Description Link
C# language design https://github.com/dotnet/csharplang
Compiler implementation https://github.com/dotnet/roslyn
Standard to describe the language https://github.com/dotnet/csharpstandard

記事集: 公式情報

記事集: よくまとまっていて参考になる

記事集: MISC

dockerで(マルチステージ)ビルドして実行したときNo frameworks were found.と言われてしまう

  • マルチステージビルド用のDockerfileRiderによる生成
  • 最初に結論: 「baseのイメージを適切に選ぶ」
  • 実際に起きたのは次のような状況
    • Mainプロジェクト: ASP.NET Core
    • Batchプロジェクト: バッチジョブ用のコンソールアプリケーション
    • BatchMainのファイルを参照している
    • Batchプロジェクトを実行しようとしたらNo frameworks were found.
  • 起きた原因: dockerのターゲットfinalに使うbaseのイメージがmcr.microsoft.com/dotnet/runtime:6.0で, mcr.microsoft.com/dotnet/aspnet:6.0ではなかったから.

dotnet-user-secrets

NuGetキャッシュの削除

1
dotnet nuget locals --clear all

インストールされているバージョンを調べる

1
dotnet --list-sdks

コマンドサンプル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dotnet new globaljson --sdk-version 6.0.400 --output <projname>
dotnet new mvc --no-https --output <projname> --framework net6.0

dotnet new sln --name <solution-name> # ソリューションファイルの生成
dotnet new sln -o <projname>
dotnet sln PartyInvites add <projname>

dotnet --list-sdks
dotnet watch
dotnet run
dotnet list package
dotnet remove package <hoge>

dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 6.0.0
dotnet ef --help
dotnet ef database drop --force --context StoreDbContext

dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli --version 2.1.113
libman init -p cdnjs
libman install bootstrap@5.1.3 -d wwwroot/lib/bootstrap
libman restore

コマンドラインでSDKのバージョンを指定する

  • プロジェクト内ではglobal.jsonでバージョンを指定する.
1
dotnet --list-sdks

ソリューション初期化

1
2
3
dotnet new sln # 現在のディレクトリに同名のファイル生成
dotnet new sln --name <MySolution> # 現在のディレクトリにファイル生成
dotnet new sln --output <MySolutionDirectory> # 指定したディレクトリに生成

ソリューションへのプロジェクト追加

1
dotnet sln add <HogeProject>

使えるテンプレートのリスト

1
dotnet new --list

バージョンを削除する: MacまたはLinuxの場合

  • dotnet --list-sdksでディレクトリを調べる
  • 該当ディレクトリを削除

バージョンを指定する

1
2
dotnet new globaljson
dotnet --list-sdks

バージョンを調べる

1
dotnet --version

パッケージを追加する

1
dotnet add package <hoge> --version

パッケージを削除する

1
dotnet remove package <hoge>

プロジェクト初期化時の参考

1
2
3
4
dotnet new globaljson --sdk-version <6.0.100> --output <MySolution/MyProject>
dotnet new web --no-https --output <MySolution/MyProject> --framework <net6.0>
dotnet new sln -o <MySolution>
dotnet sln <MySolution> add <MySolution/MyProject>

プロジェクトの参照追加

1
dotnet add reference

Emacs連携 FSAutoCompleteが導入できないとき

  • F#のファイルを開いたときに次のようなメッセージが出るとき

コマンドが誤っています

dotnet-path/to/.emacs.d/.cache/lsp/fsautocomplete/fsautocomplete.dllが見つかりません.

  • /tmp~/AppData/Local/Tempfsautocomplete1HbFmC.zipのようなファイルがダウンロードできていたので, これを~/.emacs.d/.cache/lspに展開.
  • もう一度F#のファイルを開いてLSPが発動するか確認

Emacs連携 FsAutoComplete

  • LSPからの自動インストールによく失敗する.
  • 直接公式からクローンしてビルドする.
  • 直下のglobal.jsondotnetのバージョンを合わせること.
  • dotnet tool restorefakeを入れてからdotnet fake build.
  • src/FsAutoComplete/bin/Release/net5.0にビルドされている
    • これを~/.emacs.d/.cache/lsp/fsautocompleteにコピー

EmacsでのFSACエラー: Macでの調整

1
2
3
4
5
cat /etc/dotnet/install_location

/usr/local/share/dotnet/x64
->
/usr/local/share/dotnet

fsi

trydotnet, ifsharp, Jupyter notebook with .NET Core

Try .NET, Jupyter notebook with .NET Core

要調査: これ用にインストールしたパッケージは ~/.trydotnet に置かれる?

1
2
3
dotnet tool install --global Microsoft.dotnet-interactive
dotnet interactive jupyter install
jupyter kernelspec list

IFsharp へのパッケージ導入

trydotnet でも同じ?

まとめ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#load "Paket.fsx"
Paket.Package ["Newtonsoft.Json"; "Deedle"; "MathNet.Numerics"; "MathNet.Numerics.FSharp"; ]
#load "Paket.Generated.Refs.fsx"

open System
open Deedle
open MathNet.Numerics
open Newtonsoft.Json\
open MathNet.Numerics.LinearAlgebra

load "XPlot.Plotly.Paket.fsx"
load "XPlot.Plotly.fsx""
open FSharp.Core
open XPlot.Plotly

// 以下はF# 5以降の方法:いったんコメントアウト。
//#r "nuget:Deedle"
//#r "nuget:DiffSharp"
//#r "nuget:FSharpPlus"
//#r "nuget:MathNet.Numerics"
//#r "nuget:MathNet.Numerics.FSharp"
//#r "nuget:XPlot"
ツイートからのやりとり

緩募 IFsharp で DiffSharp を使う方法. そもそもどうやって導入すればいいのかさえわからない. Powershell で dotnet add package DiffSharp --version 0.7.7 と打ったら「プロジェクトが見つかりませんでした」とか言われて怒られる. Install-Package でもうまくいかない. つらい.

そもそも .NET には一般のプログラミング言語と違いグローバルなライブラリ環境が存在しない, ということに注意が必要です. では dotnet add package や Install-Package は何をするコマンドなのかというと, 単一のプロジェクト (*.fsproj) に依存パッケージを追加するコマンドです.

一方 IFSharp は F# interactive の上に構築されているのでプロジェクトが存在しません. ではどうすればよいのかというと, ここに記載されているように #load "Paket.fsx"としてから Paket.Package もしくは Paket.Version (バージョン指定) に使いたいパッケージ名を渡し,

#load "Paket.Generated.Refs.fsx"とすることでパッケージを IFSharp の環境にインストールすることができます. 初回実行時にはパッケージの依存解決とダウンロードが走るので処理に時間がかかります. また Paket を使うセルは他の処理と分けておいた方がよいです (補完を利かせるため).

vscode連携: ツールチップなどが出てこない

  • F#用のディレクトリだけを開けばよさそう

vscode連携: やたらエラーが出る

  • 参考
  • エラーを出したディレクトリに .nuget/packages/ へのシンボリックリンクを張れば良い(?)
  • 自分用参考: コマンドプロンプトで mklink myhome\junk\fsharp\packages myhome\.nuget\packages
    • myhome ホームディレクトリへのフルパス: 使うマシンごとに切り替えよう.
    • Windows のコマンドプロンプトだから, ディレクトリの区切りは \ で.

TODO Windowsでの英語化

  • 参考: URL
  • Visual Studio Installerで適当に設定を眺めてみる

アップデート

  • 2022/12時点では公式からバイナリインストールするしかない模様.

アンインストール: Mac

/tmp直下

事前にバージョン(curlでのダウンロード先)を確認すること.

1
2
3
4
5
6
7
cd /tmp
curl -OL https://github.com/dotnet/cli-lab/releases/download/1.6.0/dotnet-core-uninstall.tar.gz
mkdir -p /tmp/dotnet-core-uninstall
tar -zxf dotnet-core-uninstall.tar.gz -C dotnet-core-uninstall
cd dotnet-core-uninstall
dotnet-core-uninstall list
./dotnet-core-uninstall -h

~/tmp直下

事前にバージョン(curlでのダウンロード先)を確認すること.

1
2
3
4
5
6
7
cd ~/tmp
curl -OL https://github.com/dotnet/cli-lab/releases/download/1.6.0/dotnet-core-uninstall.tar.gz
mkdir -p ~/tmp/dotnet-core-uninstall
tar -zxf dotnet-core-uninstall.tar.gz -C dotnet-core-uninstall
cd dotnet-core-uninstall
dotnet-core-uninstall list
./dotnet-core-uninstall -h

インストール: Windows

  • Build Tools for Visual Studio 2019
    • 適宜最新版を選ぶ
  • インストール対象を選ぶ画面では「.NET デスクトップ ビルドツール」をチェック
  • 右側に表示されるオプションで「F# コンパイラ」をチェック
  • パス指定
    • C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\CommonExtensions\Microsoft\FSharp

インストール: M1 Mac

  • ビルド時に必要なことがあり, Arm版だけではなくx64版もインストールする.

インストール: Linux

  • Use F# on Linux
  • Debian(Chromebook上のLinux)にはDebian版があるので, 適切なOSでのインストール法を選ぶこと.
  • 細かい記述は変わるためここではコマンド詳細は記録しない: 公式情報を確認すること.
  • dotnet-install.shを使うとき
    • バージョン指定は--channel--versionがある.
    • 適切な方を使うとよいが2022-02-09のChromebook作業時はFsAutoCompleteなどの関係で--version 6.0.100を利用
    • dotnet tool restore
    • dotnet fake buildできるようになる.

ソリューションへのプロジェクト追加

  • テストプロジェクトはprojname.Test.Unit, projname.Test.Integrationなどが標準.
1
2
3
dotnet new xunit -o <project-name> -f net6.0
dotnet sln add <project-name>
dotnet add <project-name> reference <solution-name>

テストのメソッド名の作成指針

  • Method_Condition_Expectation
  • Method_Should_When

ASP.NET

AWS: 透過的にSSMパラメータストアを利用する

launchSetting.jsonappSetting.json

  • URL
  • launchSetting.json: 起動時のプロファイル設定
    • ここで設定された起動プロファイルはdotnet run --launch-profile <プロファイル名>で実行できる
    • ASPNETCORE_ENVIRONMENT環境変数の設定値はIHostingEnvironment.EnvironmentNameプロパティから参照可能.
    • IHostingEnvironmentの拡張メソッドIsDevelopment, IsStaging, IsProduction, IsEnvironmentで現在の環境に応じた処理を記述可能.
  • appSetting.json: アプリの構成情報
    • ASPNETCORE_ENVIRONMENT環境変数で参照するappSettings.jsonを変更可能

読書メモ, 2022 MASTERING MINIMAL APIS IN ASPNET CORE

1章

1
dotnet --list-sdk

2章

  • P.38
1
dotnet tool install -g LiveReloadServer
  • P.39, OpenAPIサポートのためにNuGetのSwashbuckle.AspNetCoreがある
1
2
3
4
5
6
7
8
9
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

3章

  • P.58, options pattern within an ASP.NET applicationは何か?
  • P.59, .NET Coreから設定ファイルはweb.configからappsettings.jsonになった.
  • P.59, IConfigurationで与えられたオブジェクトでappsettingsの設定を読める.
  • P.59, 他には次の対象から設定を取得する
    • 参考
    • Environment variables
    • Azure Key Vault
    • Azure App Configuration
    • Command-line arguments
    • Custom providers, installed or created
    • Directory files
    • In-memory .NET objects
  • P.71 docker-compose.override.yaml

M1 Macでのキーチェーン要求を解除する

  • URL
  • 単純にPCログイン用のパスワードを入力し, ダイアログ内の「常に許可」を選択すればよい.

開発中にhttpsを使う

1
2
dotnet dev-certs https --clean
dotnet dev-certs https --trust

ローカルでProductionを実行

1
dotnet run --environment Production

開発環境での環境変数の設定

  • URL
  • Properties/launchSettings.jsonで設定すればよい.
  • 特にAWSでの公開を前提にするならAWSでは環境変数に接続文字列の情報が格納されるため, はじめから同じ形式で環境変数を設定しておくとよい.

環境変数から値を読み込む方法

環境変数を設定する

1
dotnet run --environment Production

シークレットをPOCOに設定する

1
2
3
4
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
1
2
3
var moviesConfig =
    Configuration.GetSection("Movies").Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;
1
2
3
4
5
public class MovieSettings
{
    public string ConnectionString { get; set; }
    public string ServiceApiKey { get; set; }
}

初期化

1
2
dotnet tool restore
dotnet restore

短縮URL作成

導入メモ: global.json

  • バージョンを適切に設定すること.
1
2
3
4
5
{
  "sdk": {
    "version": "6.0.400"
  }
}

導入メモ: docker-compose.yml

  • バージョンは都度修正すること.
  • .envは開発サンプル用設定として共通にしている: 必要に応じて適切な値を設定すること.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PORT=3000

DB_ROOT_HOST="%"
DB_ROOT_USER=postgres
DB_ROOT_PASS=root
POSTGRES_USER=user
DB_USER=user
DB_PASS=pass
DB_PORT=5432
DB_NAME=mydb
TZ=Asia/Tokyo
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
version: "3"
services:
  pgsql:
    image: postgres:14
    platform: linux/x86-64
    tty: true
    env_file: .env
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
      TZ: ${TZ}
    ports:
      - ${DB_PORT}:5432
    # volume:
    #   - ./db/init:/docker-entrypoint-initdb.d
    healthcheck:
      test: [ "CMD", "pg_isready", "-U", "${POSTGRES_USER}", "||", "exit", "1" ]
      interval: 2s
      timeout: 5s
      retries: 5

導入メモ: docker-compose.with-dotnet.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PORT=3000

DB_ROOT_HOST="%"
DB_ROOT_USER=postgres
DB_ROOT_PASS=root
POSTGRES_USER=user
DB_USER=user
DB_PASS=pass
DB_PORT=5432
DB_NAME=mydb
TZ=Asia/Tokyo
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
version: "3"
services:
  pgsql:
    image: postgres:14
    platform: linux/x86-64
    tty: true
    env_file: .env
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
      TZ: ${TZ}
    ports:
      - ${DB_PORT}:5432
    healthcheck:
      test: [ "CMD", "pg_isready", "-U", "${POSTGRES_USER}", "||", "exit", "1" ]
      interval: 2s
      timeout: 5s
      retries: 5
  backend:
    image: "mcr.microsoft.com/dotnet/sdk:6.0"
    volumes:
      - ./backend:/app
    command: dotnet watch --project ./app run --urls "http://0.0.0.0:80"
    ports:
      - "80:80"

導入メモ: ツールのインストール

  • ローカルインストールのEF Coredotnet dotnet-efコマンドで呼び出す
  • バージョンは適宜最新化すること
1
2
dotnet new tool-manifest
dotnet tool install dotnet-ef --version 6.0.14
  • EF Coreをグローバルインストールしていて, グローバルのツールのバージョンを合わせたい場合は次のコマンドを発行する.
1
2
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 6.0.14

導入メモ: パッケージのインストール

1
2
dotnet add package Microsoft.EntityFrameworkCore --version 6.0.14
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 6.0.13
  • 必要に応じてEF Coreは次のパッケージを利用
1
2
3
4
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 6.0.13
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 6.0.8
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 6.0.13 # scaffoldで必要
  • 必要に応じて次のパッケージを利用
1
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design -v 6.0.11

導入メモ: Program.csへの各データベース設定

SQLite

1
2
3
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ??
                      throw new InvalidOperationException("Connection string 'DefaultConnection' not found.")));

In Memory

1
2
builder.Services.AddDbContext<MyDbContext>(opt =>
    opt.UseInMemoryDatabase("mydb"));

PostgreSQL

1
2
3
builder.Services.AddDbContext<MyDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("MyDbContext") ??
                      throw new InvalidOperationException("Connection string 'MyDbContext' not found.")));

導入メモ: appsettings.jsonへの各データベース設定

SQLite

1
2
3
  "ConnectionStrings": {
    "DefaultConnection": "DataSource=Migrations/app.db;Cache=Shared"
  },

SQLiteメモリ

1
2
3
  "ConnectionStrings": {
    "DefaultConnection": "DataSource=:memory:"
  },

PostgreSQL

1
2
3
  "ConnectionStrings": {
      "DefaultConnection": "User ID=user;Password=pass;Host=localhost;Port=5432;Database=mydb;"
  },

導入メモ: フロントエンド用ライブラリ導入, LibManでのインストール

  • MacでLibManを使うときはexport PATH="$PATH:/Users/username/.dotnet/tools"を設定すること.
  • これでうまくいかない場合は次の通り.
1
2
dotnet new tool-manifest
dotnet tool install Microsoft.Web.LibraryManager.Cli
  • dotnet libmanで実行
  • LibMan (Microsoft.Web.LibraryManager.Cli)は(ローカル)インストール済みとする.
  • wwwroot/libを削除する.
1
2
3
4
5
6
7
dotnet libman init -p cdnjs
dotnet libman install bootstrap --provider cdnjs --destination wwwroot/lib/bootstrap
dotnet libman install jquery --provider cdnjs --destination wwwroot/lib/jquery
dotnet libman install jquery-validate --provider cdnjs --destination wwwroot/lib/jquery-validation
dotnet libman install jquery-validation-unobtrusive  --provider cdnjs --destination wwwroot/lib/jquery-validation-unobtrusive
dotnet libman install font-awesome --provider cdnjs --destination wwwroot/lib/font-awesome
dotnet libman install @fluentui/web-components --provider cdnjs --destination wwwroot/lib/fluentui/web-components
  • ViewsまたはPagesの参照を正す.
  • フォーマッターをかける.

導入メモ: データベースのスキャフォールド

  • EF Coreの「リバースエンジニアリング・データベースファースト・スキャフォールド」参照

導入メモ: コントローラーのスキャフォールド

  • Rider(またはVisual Studio)のコントローラースキャフォールドで対応
    • 実質的にはdotnet-aspnet-codegeneratorを発行している
    • 具体的には次のようなコマンドを発行する
1
2
dotnet tool install dotnet-aspnet-codegenerator --version 6.0.11
dotnet dotnet-aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers

導入メモ: MVCとAPIの共存

  • MVCを基礎に構築しているとする
  • dotnet add package Swashbuckle.AspNetCoreを導入する
  • Program.csに追記する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// API利用:`Swashbuckle.AspNetCore`が必要
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "ToDo API",
        Description = "An ASP.NET Core Web API for managing ToDo items",
        TermsOfService = new Uri("https://phasetr.com/archive"),
        Contact = new OpenApiContact
        {
            Name = "Example Contact",
            Url = new Uri("https://phasetr.com/contact")
        },
        License = new OpenApiLicense
        {
            Name = "Example License",
            Url = new Uri("https://phasetr.com/archive")
        }
    });
});

// 中略: `var app = builder.Build();`のあと

// API利用:開発時は`Swagger`を立ち上げる
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// API利用
app.MapControllers();
  • コントローラーには次のディレクティブを追加する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
using Microsoft.AspNetCore.Mvc;
using HogeProject.Models;

namespace HogeProject.Controllers.Api.V1;

// 次のディレクティブの追加: これでAPI呼び出しでき, `Swagger`にも反映される
[ApiController]
[Route("api/v1/[controller]")]
public class HogeController : ControllerBase
{
    // 本体
}

導入メモ: EF Coreのマイグレーション

  • EF Coreのマイグレーションの記述を参照

導入メモ: テストプロジェクトを作る

1
2
3
4
5
dotnet new xunit -o <Proj.Tests>
dotnet sln add <Proj.Tests/Proj.Tests.csproj
dotnet add <Proj.Tests/Proj.Tests.csproj> reference <Proj/Proj.csproj>
cd Proj.Tests
dotnet add package Moq
  • 具体的には次のように書く
1
2
3
4
5
dotnet new xunit -o CityBreaks.Tests
dotnet sln add CityBreaks.Tests/CityBreaks.Tests.csproj
dotnet add CityBreaks.Tests/CityBreaks.Tests.csproj reference CityBreaks/CityBreaks.csproj
cd CityBreaks.Tests
dotnet add package Moq

Program.cs WebApplicationBuilder

  • var builder = WebApplication.CreateBuilder(args);
  • IApplicationBuilder: アプリケーションのリクエストまたはミドルウェア パイプラインの構成を許可
  • IEndpointRouteBuilder: 受信リクエストを特定のページにマッピングする構成を有効化
  • IHost: アプリケーションを開始および停止する手段を提供

Program.cs WebApplicationBuilderのプロパティ

  • Environment: アプリケーションが実行されているWebホスティング環境に関する情報を提供
  • Services: アプリケーションのサービスコンテナを表す
    • builder.Services.AddRazorPages();
  • Configuration: 構成プロバイダーの構成を有効化
  • Logging: ILoggingBuilder経由でロギング構成を有効化
  • Host: サードパーティのDIコンテナーを含むアプリケーションホスト固有のサービスの構成をサポート
  • WebHost Webサーバー構成を有効化
    • builder.WebHost.UseWebRoot("content");

Program.cs 環境ごとにDIの設定を変える

1
2
3
4
5
6
7
8
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddTransient<IEmailSender, EmailService>();
}
else
{
    builder.Services.AddTransient<IEmailSender, ProductionEmailService>();
}

Authentication

ExceptionHandler

Identity: LockoutOptions

  • サインインの試行が複数回失敗した場合にアカウントのロックアウトを有効化できる
  • ブルートフォース攻撃への対処に使える

Identity: AspNetUserRolesへのシード登録

  • StackOverFlowに投稿.
  • できた: GitHubのリポジトリ.
  • メモ
    • 動かなかったときのコードで要点はModels/ApplicationUserRole.cspublic class ApplicationUserRole : IdentityUserRole<Guid>
    • Guidの型不整合(?)で新たにApplicationUserRoleが作られてしまった模様
    • public class ApplicationUserRole : IdentityUserRole<string>に変えたら新たなテーブルも生成されず, 正しくデータが投入できた.

How to seed AspNetUserRoles in ASP.NET Core 6 and EFCore 6

My sample project is here.

I have several related problems, but the main problem is seeding to AspNetUserRoles. I would like to add fundamental data, and I can seed data to AspNetUsers and AspNetRoles. However the same approach seems inappropriate to AspNetUserRoles. ApplicationUserRole class creates the table ApplicationUserRole, and is not assigned to AspNetUserRoles. This is the 1st problem.

The second problem is configuration to ApplicationUserRole. I do not give the same properties as AspNetUserRoles, for example, ApplicationUserRole does not have foreign keys. I looked up some official pages, e.g. here or here, and wrote some more codes. However I do not have proper codes. Of course, my final goal is proper seeding to AspNetRoles, but the code for ApplicationUserRole has something wrong, I think.

Thanks in advance.

My sample project is here.

I followed here, and created ApplicationUser and ApplicationRole class. This seems to be proper. But ApplicationUserRole class seems to be improper. I checked the page, but I have the error,

The entity type 'IdentityUserRole' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.

Identity: Claimとセッションの変更

  • 参考
  • 次のようなコードを書けばよい.
    • Claimを削除・追加する形でClaim自体を更新.
    • 最後に_signInManager.RefreshSignInAsync(user)Claimをセッションに反映.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public async Task<IActionResult> OnPostChangeMainShopAsync(int shopId)
{
    var isProperShopId = await IsProperShopId(shopId);
    if (!isProperShopId) return NotFound();
    var userName = User.Identity!.Name!;
    var user = await _applicationUserService.GetByUserNameAsync(userName);
    user.ShopId = shopId;
    await _applicationUserService.UpdateAsync(user);
    var identity = (ClaimsIdentity) User.Identity;
    var claim = identity.FindFirst("ShopId");
    if (claim == null) return RedirectToPage("./Details", new {shopId});
    identity.RemoveClaim(claim);
    identity.AddClaim(new Claim("ShopId", $"{shopId}"));
    await _signInManager.RefreshSignInAsync(user);
    return RedirectToPage("./Details", new {shopId});
}

Identity: Roleによる権限管理

Identity: スキャフォールド

  • URL
  • 必要に応じてバージョン指定でインストールしよう.
1
2
3
4
5
6
7
8
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

dotnet tool install -g dotnet-aspnet-codegenerator
  • ヘルプ確認
1
dotnet aspnet-codegenerator identity -h
  • あとはRiderまたはVisual Studioでスキャフォールドするとよい.
  • Visual Studioでの参考URL

Identity: パスワードへの制約

  • Program.csbuilder.Services.AddDatabaseDeveloperPageExceptionFilter();の下あたりに次の設定を追加する
1
2
3
4
5
6
7
8
9
    builder.Services.Configure<IdentityOptions>(options =>
    {
        options.Password.RequireDigit = false;
        options.Password.RequireLowercase = false;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequiredLength = 8;
        options.Password.RequiredUniqueChars = 1;
    });

Identity: パスワードハッシュの値を固定する

  • URL
  • 書いた時点では返事待ち: あとで書き直す

How to unchange PasswordHash with seeding in ASP.NET Core 6 and EF Core 6

My sample is here, and, in particular, I set IdentityUser.PasswordHash like Hasher.HashPassword(new ApplicationUser(), "phasetrdevadmin").

Hashed password values in a snapshot file change every migration, but I do not like this behavior. I need more codes, such as salting, I think. How can I do unchange hashed password values?

Identity: 認証追加

  • URL
  • Program.csでフォルダーレベルの一括設定がある

Identity: モデルのカスタマイズ

  • URL
  • IdentityUserとなっている部分を適切に継承したクラスに置き換える必要がある.

Identity: ユーザーのIDを取得する

1
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

Identity: ユーザーのキーの型をintに変更

1
2
3
public class ApplicationUser : IdentityUser<int>
{
}
  • なくてもいいがついでにApplicationRoleも作る
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class ApplicationRole : IdentityRole<int>
{
}

public enum UserRoles
{
    Admin,
    Staff,
    Customer
}
  • ApplicationDbContextクラスをカスタマイズしてApplicationUserクラスを使うようにする.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<ApplicationUser> ApplicationUsers => Set<ApplicationUser>();
    public DbSet<ApplicationRole> ApplicationRoles => Set<ApplicationRole>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder
            .ApplyConfiguration(new ApplicationUserConfiguration())
            .ApplyConfiguration(new ApplicationRoleConfiguration())
    }
}
  • Program.csに設定を追加する.
    • ここで最初の記事を参照した.
    • AddIdentity<ApplicationUser, ApplicationRole>をしないとApplicationRoleの初期値が入らなかった.
1
2
3
4
5
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
        options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

Identity: ユーザーごとのRole取得

  • 参考
  • UserManeger.GetRoles(TUser)またはUserManeger.GetRolesAsync(TUser)を使えばよい.

Identity: ユーザー名への一意制約

  • IdentityUser(または継承)クラスに対して[Index(nameof(UserName), IsUnique = true)]のようなアノテーションをつける

JWT: dotnet user-jwts

libmanでインストールしたツールをリストア

1
dotnet libman restore

Minimal API: ログを仕込む

1
2
3
4
5
app.MapGet("/", (ILogger<Program> logger) =>
{
    logger.LogInformation("👺 access to /");
    return "This is a GET.";
});

OpenAPI(Swagger)に認証を導入

  • URL
  • コードを一通り書いたあとにサーバーを再起動するとOpenAPIのUIにAuthorizeボタンが追加される

QRコードの生成

  • 2023/01/20確認
  • 参考
  • Base64で作ってHTMLを使って表示する
  • Razor Pagesで作るとする
  • dotnet add package QRCoderを実行
  • cshtml.csに次のように書く: 適当に補いつつ修正すること
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using QRCoder;

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public string ImageSrc { get; set; } = string.Empty;

    public IActionResult OnGet()
    {
        _logger.LogInformation("LOG TEST");
        var text = "https://phasetr.com/archive";
        var qrGenerator = new QRCodeGenerator();
        var qrCodeData = qrGenerator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q);
        var qrCode = new PngByteQRCode(qrCodeData);
        var bytes = qrCode.GetGraphic(10);
        var base64Str = Convert.ToBase64String(bytes);
        ImageSrc = $"data:image/png;base64,{base64Str}";
        return Page();
    }
}
  • cshtmlに次のように書く
1
2
3
4
<div class="text-center">
    <h2 class="display-4">Welcome</h2>
    <img src="@Model.ImageSrc" alt=""/>
</div>

Razor Class Library (RCL)

  • URL
  • 標準では~/Areas/Pagesに置く
  • ~/Pagesでコンテンツを公開するRCLを作成する場合は上記ページの「RCLページのレイアウト」参照
  • RCLRazor Pagesを使う場合はホスティングアプリでRazor Pagesサービスとエンドポイントを有効化する
1
2
builder.Services.AddRazorPages();
app.MapRazorPages();

Razor Pages @page

  • ルートテンプレートはURLと同じように動作する
  • パスセパレーターで始まらないなら現在のページに対して相対的
  • パスセパレーターで始まるなら絶対的
  • @page "{cityName}/{arrivalYear}-{arrivalMonth}-{arrivalDay}"のような指定もありうる
  • 指定の注意
    • @page "{cityName}": 必ず何か値を指定しなければならず, 素のアクセスだと404が出る
    • @page "{cityName=paris}" デフォルト値指定
    • @page "{cityName?}": 無指定でもよい

Razor Pages @page キャッチオールパラメータ

  • {*cityName}: 生成されたURL内のパス区切り文字がURLエンコードされる
    • /City/London/2022/4/18/City%2FLondon%2F2022%2F4%2F18としてレンダリングされる
  • {**cityName}: エンコーディングがデコード・ラウンドトリップされる
    • 、生成されたURLにリテラルパス区切り文字が含まれる
    • /City/London/2022/4/18

Razor Pages @page ルート制約

  • "{cityName}/{arrivalYear:int}-{arrivalMonth:int}-{arrivalDay:int}"
    • {key:int}:intの部分
  • "{cityName}/{arrivalDate:datetime}"
  • 次のように有効な値の範囲指定もある
    • "{cityName}/{arrivalYear:int}-{arrivalMonth:range(1-12)}-{arrivalDay:int}"
  • 制約の一覧
  • いくつかの説明は次の通り
    • alpha: 大文字または小文字のラテン アルファベット文字 (az または AZ) に一致
    • bool: ブール値に一致
    • int: 32ビット整数値に一致
    • datetime: DateTime値に一致
    • decimal: 10進数値に一致します
    • double: 64ビットの浮動小数点値に一致
    • float: 32ビットの浮動小数点値に一致
    • long: 64ビット整数値に一致
    • guid: GUID値に一致
    • length: 指定された長さまたは指定された長さの範囲内の文字列に一致
      • cf. {key:length(8)}, {postcode:length(6,8)}
    • min: 最小値を持つ整数に一致
      • cf. {age:min(18)}
    • max: 最大値を持つ整数に一致
      • cf. {height:max(10)}
    • minlength: 最短の文字列に一致
      • cf. {title:minlength(2)}
    • maxlength: 最長の文字列に一致
      • cf. {postcode:maxlength(8)}
    • range: 値の範囲内の整数に一致
      • cf. {month:range(1,12)}
    • regex: 正規表現に一致
      • cf. {postcode:regex(^[A-Z]{2}\d\s?\d[A-Z]{2}$)}

Razor Pages addTagHelperディレクティブ

  • 2つの引数を取る
    • 有効にするタグヘルパー
    • 有効にするタグヘルパーを含むアセンブリ名
  • ワイルドカード文字(*)は指定されたアセンブリ内のすべてのタグヘルパーを有効にする必要があることを指定
  • フレームワークタグヘルパー: Microsoft.AspNetCore.Mvc.TagHelpers

Razor Pages asp-page-handler, 同じページ内で複数のフォームがある場合

  • formタグにasp-page-handlerを指定すると, その値に対してハンドラーOnPostHogeOnGetFugaを呼び出せる.
  • 特にクエリ文字列に?handler=Hogeなどを追加する.

Razor Pages AutoMapper

  • 公式
  • 大量のモデルバイディングで便利なライブラリ
  • 2022年時点ではASP.NET Core Razor Pages in Action 2nd editionで推奨

Razor Pages HTMLエンコードしたくない場合

  • 基本は全てHTMLエンコードされる
1
2
3
4
5
6
7
8
9
@{
    Layout = "_Layout";
    const string output = "<p>This is a paragraph.</p>";
}

<div>
    <p>@output</p>
    <p>@Html.Raw("<p>This is a paragraph.</p>")</p>
</div>

Razor Pages HTMLエンコードレベルを下げる, WebEncoderOptions

  • キリル文字・中国語・アラビア語などの非ラテン語ベースの言語では, すべての文字が対応するHTMLにエンコードされるため, 生成されるソースコードの文字数が大幅に増加する可能性がある.
  • 指定の参考
    • UnicodeRanges.Allを使うのも一手.
1
2
3
4
builder.Services.Configure<WebEncoderOptions>(options =>
{
   options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Latin1Supplement);
});

Razor Pages POSTでリクエストが飛ばない

  • 次の可能性を検討する
    • formタグのaction属性またはmethod属性の指定.
    • フォームフィールドのname属性の設定
    • モデルのバリデーションに失敗している.
    • OnPostのハンドラーが存在しているか.
    • フォームに必要なAntiforgeryトークンがない. トークン設定は次の通り.
1
<div asp-validation-summary="ModelOnly" class="text-danger"></div>

Razor Pages removeTagHelperディレクティブ: 特定のタグの処理を選択的にオプトアウト

1
2
3
4
5
@removeTagHelper "Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers"
<!a href="https://www.learnrazorpages.com">Learn Razor Pages</!a>

@tagHelperPrefix x:
<x:a asp-page="/Index">Home</x:a>

Razor Pages _Layoutを探す場所

  • 基本は_ViewStart.cshtmlに指定するとよい: 全ての処理に先立って読まれる
  • 単にLayout = “_Layout”;とだけ書いて, 呼び出しページを\Pages\Admin\DestinationsOrdersに配置したときの検索場所は次の通り
    • \Pages\Admin\DestinationsOrders\ _Layout.cshtml
    • \Pages\Admin\ _Layout.cshtml
    • \Pages\_Layout.cshtml
    • \Pages\Shared\ _Layout.cshtml
    • \Views\Shared\ _Layout.cshtml

Razor Pages URLの処理, スラッグなど

  • 2022.ASP.NET_Core_Razor_Pages_in_Action.pdf, Chap 4

Razor Pages ViewBag

  • ViewDataへの別のアクセス方法だが非推奨
  • ViewModel中では使えない
  • Razorファイルでだけ使える
  • プロパティはラッパーで, キーに一致するプロパティ名を介して項目にアクセスする

Razor Pages ViewData

  • 型づけも弱く, 利用は推奨されない
  • ページタイトルなど小さく単純なデータをレイアウトページに渡す場合に便利
  • 辞書ベースの機能
  • 要素はViewDataDictionaryキーと値のペアとして, 大文字と小文字を区別しない文字列キーを参照してアクセスする
1
2
3
ViewData["Title"] = "Welcome!";

<title>@ViewData["Title"] - WebApplication1</title>

Razor Pages View Model

  • ビューモデルとしての役割とコントローラーとしての役割がある
  • ビューモデル: ビューに必要なデータだけのコンテナー
  • コントローラー

Razor Pages View Model ハンドラーメソッドのパラメーター

  • ?id=5の形式で渡る: intだと初期値は0
  • ハンドラーメソッドはオーバーロードできない
  • 受信データをパラメータに一致させる手法はモデルバインディング
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@{
    Layout = "_Layout";
}

<div>
    <p>@Model.Message</p>
</div>


public class Index : PageModel
{
    public string Message { get; set; } = string.Empty;
    public void OnGet(int id)
    {
        Message = $"OnGet executed with id = {id}";
    }
}

Razor Pages View Model 名前つきハンドラーメソッド

  • フォームが二つあって区別して使いたい場合もある
  • asp-page-handlerOnPostHogeOnPostFugaなどを使う
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@page
@model WelcomeModel
@{
}

<div class="col">
    <form method="post" asp-page-handler="Search">
        <p>Search</p>
        <input name="searchTerm" />
        <button>Search</button>
    </form>

    <form method="post" asp-page-handler="Register">
        <p>Register</p>
        <input name="email" />
        <button>Register</button>
    </form>
    <p>@Model.Message</p>
</div>

public class WelcomeModel : PageModel
{
    public string Message { get; set; }
    public void OnPostSearch(string searchTerm)
    {
        Message = $"You searched for {searchTerm}";
    }

    public void OnPostRegister(string email)
    {
        Message = $"You registered {email} for newsletters";
    }
}

Razor Pages Blazor WebAssemblyとの共存

  • dotnet new blazorwasm -hoでプロジェクトが初期化できる
  • .NET 7時点ではClient, Server, Sharedの三つのプロジェクトができる.
    • 特にServerRazor Pagesになっている
1
dotnet new blazorwasm -ho --auth Individual --pwa -o <SolutionName>
  • Serverdotnet watchなど適当な実行コマンドを実行すればWasm連携で立ち上がる

Razor Pages アクションの結果

  • PageResult
    • ヘルパーメソッド: Page
    • 現在のRazorページをレンダリングする
  • FileContentResult
    • ヘルパーメソッド: File
    • バイト配列、ストリーム、または仮想パスからファイルを返す
  • NotFoundResult
    • ヘルパーメソッド: NotFound
    • リソースが見つからなかったことを示すHTTP 404ステータスコードを返す
  • PartialResult
    • Partial
    • 部分的なビューまたはページをレンダリングする
  • RedirectToPageResult
    • RedirectToPage, RedirectToPagePermanent
    • ユーザーを指定されたページにリダイレクトする
    • RedirectToPage: 一時的なリダイレクトの302を表す.
  • StatusCodeResult
    • StatusCode
    • 指定されたステータスコードでHTTP応答を返す

Razor Pages カスタム検証属性

  • 2022.ASP.NET_Core_Razor_Pages_in_Action.pdf, 5.3.5

Razor Pages コメントの書き方

  • @**@で囲む

Razor Pages タグヘルパーでのセレクトリストの生成

1
2
3
4
5
6
@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">
    <select asp-for="Country" asp-items="Model.Countries"></select>
    <br /><button type="submit">Register</button>
</form>

Razor Pages ビューコンポーネント

  • ファイル・データベース・Webサービスなどの外部リソースへの呼び出しなど, 結果のHTMLスニペットに含めるデータを取得または処理するために何らかのタイプのサーバー側ロジックが必要な場合, 部分ページではなくビュー コンポーネントが便利

Razor Pages 標準コードブロックとfunctionsコードブロックの違い

  • 関数ブロック: publicメンバーの宣言をサポート
  • 標準コードブロック: サポートされていない

Razor Pages フィルター, IAsyncPageFilter

  • 参考記事
  • ASP.NET Core MVCではIActionFilter: 要求処理パイプラインのアクション メソッドが呼び出される直前・直後に独自のコードを実行させられる
  • Razor PagesでもIAsyncPageFilterインターフェースがある
    • 同期版のIPageFilterインターフェースもある.
  • Filters/AsyncPageFilter.csを次のように実装
    • namespaceなどは適切に設定
    • ここではCitybreaksに合わせてSerilogにしている.
    • ASP.NET Core標準のロガーでよい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Microsoft.AspNetCore.Mvc.Filters;

namespace CityBreaks.Filters;

public class AsyncPageFilter : IAsyncPageFilter
{
    private readonly Serilog.ILogger _logger;

    public AsyncPageFilter(Serilog.ILogger logger)
    {
        _logger = logger;
    }

    public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        var httpContext = context.HttpContext;
        _logger.Information(
            "HttpType: {Method}|Path :{Path}\tUserAgent: {UserAgent}\tPage: {PageName}\tHandler: {Handler}",
            httpContext.Request.Method, httpContext.Request.Path, httpContext.Request.Headers[";User-Agent"].ToString(),
            context.HandlerInstance.GetType().Name, context.HandlerMethod?.MethodInfo.Name);
        return Task.CompletedTask;
    }

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
        PageHandlerExecutionDelegate next)
    {
        await next.Invoke();
    }
}
  • 上記元記事では次のように実装している.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Microsoft.AspNetCore.Mvc.Filters;

namespace WebAppSample;

public class MyAsyncPageFilter : IAsyncPageFilter
{
    private readonly ILogger _logger;
    public MyAsyncPageFilter(ILogger logger)
    {
        _logger = logger;
    }

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
    {
        await next.Invoke();
    }

    public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        var httpContext = context.HttpContext;
        var message = $"HttpType: {httpContext.Request?.Method}|Path :{httpContext.Request?.Path}\tUserAgent: {httpContext.Request?.Headers["User-Agent"].ToString()}\tPage: {context.HandlerInstance?.GetType().Name}\tHandler: {context.HandlerMethod?.MethodInfo?.Name}";
        _logger.LogTrace(message);
        return Task.CompletedTask;
    }

}
  • Program.csで次のように設定
    • ここでもCitybreaksに合わせてSerilogで書いている.
1
2
3
4
5
6
7
    builder.Services.AddRazorPages(options =>
    {
        // 省略
    }).AddMvcOptions(options =>
    {
        options.Filters.Add(new AsyncPageFilter(Log.Logger));
    });
  • 上記元記事では次のように実装している.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
using WebAppSample;

var builder = WebApplication.CreateBuilder(args);

var logger = LoggerFactory.Create(config =>
{
    config.AddConsole();
    config.SetMinimumLevel(LogLevel.Trace);
}).CreateLogger("WebAppSample");

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.Filters.Add(new MyAsyncPageFilter(logger));
    });


var app = builder.Build();

... 以下省略 ...
  • 見やすくするためにappsettings.Development.jsonを書き換えてフレームワーク側が出すログレベルを変える.
1
2
3
4
5
6
7
8
9
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning"
    }
  }
}
  • あとはdotnet watchで実行

Razor Pages 部分ビュー

  • @pageディレクティブがないファイル
  • ふつうのビューと違って_ViewStart.cshtmlを呼び出さず, _Layout.cshtmlのレイアウトは適用されない
  • 規則ではないが, たいてい_myPartial.cshtmlのようにファイル名の先頭にアンダースコアを付ける
  • <partial name=”_NavigationPartial” />で呼び出す
  • 親ビューから部分ビューにViewDataを渡す場合はview-data属性にViewData名を指定する
  • モデルを渡す場合はfor属性または{model}属性に渡したいモデル名を指定する.
1
<partial name="_ProductViewDataPartial" for="Product" view-data="sampleViewData">

Razor Pages ページごとにテンプレートの一部だけ書き換える

  • RenderSectionAsyncメソッドをコンテンツを表示する場所に配置
    • @await RenderSectionAsync(“ThingsToDoWidget”, false)と第二引数をfalseにしておくとよい
  • 各ページで@sectionディレクティブを使う
  • ページ固有のJavaScriptファイル読み込みにも使える
  • 参考メソッド
    • IsSectionDefined
    • IgnoreSection

Razor Pages ページモデル [DataType]属性

  • データバインディング・検証に使える

Razor Pages 文字エンコーディングの変更

  • ASP.NET in Action, 3章
  • WebEncoderOptions

Razor Pages モデルバインディング

  • GETメソッドに対しては[BindProperty(SupportsGet=true)]まで指定する必要がある
  • HTMLタグのnameにハイフンがあるe-mailなどは[BindProperty(Name="e-mail")]のように書く必要がある
  • クラス自体に[BindProperties]をつけてもよい
    • オーバーポスティング攻撃があるため安易にクラスにつけてはならない
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    [BindProperty] public string CityName { get; set; } = string.Empty;
    [BindProperty(SupportsGet=true)] public int Id  { get; set; } = 0;
    [BindProperty(Name="e-mail")] public string Email { get; set; } = "";


<div class="col-4">
    <h3>モデルバインディング</h3>
    <form method="post">
        <div class="mb-3">
            <label for="name">Enter city name</label>
            <input class="form-control" type="text" name="cityName"/>
        </div>
        <button class="btn btn-primary">Submit</button>
    </form>
    @if (Request.HasFormContentType && !string.IsNullOrWhiteSpace(Model.CityName))
    {
        <p>You submitted @Model.CityName</p>
    }
</div>

Razor Pages UserClaimのカスタマイズ

  • URL
  • Factoryを継承して適切に設定する: 必要に応じてApplicationRoleも追加する
    • ApplicationUser, ApplicationRoleはそれぞれIdentityUserIdentityRoleの拡張.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class MyUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
    public MyUserClaimsPrincipalFactory(
        UserManager<ApplicationUser> userManager,
        IOptions<IdentityOptions> optionsAccessor)
        : base(userManager, optionsAccessor)
    {
    }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);
        // 追加したい要素を文字列で設定する
        identity.AddClaim(new Claim("ContactName", user.ContactName ?? "[Click to edit profile]"));
        return identity;
    }
}
  • ApplicationRole追加版は下記参照.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
{
    private readonly IApplicationUserService _applicationUserService;

    public MyUserClaimsPrincipalFactory(
        UserManager<ApplicationUser> userManager,
        RoleManager<ApplicationRole> roleManager,
        IApplicationUserService applicationUserService,
        IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
    {
        _applicationUserService = applicationUserService;
    }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);
        // 追加したい要素を文字列で設定する
        identity.AddClaim(new Claim("ContactName", user.ContactName ?? "[Click to edit profile]"));
        return identity;
    }
}
  • Program.csDI追加.
1
2
3
4
5
6
7
8
public void ConfigureServices(IServiceCollection services)
{
    .     .     .     .      .
    services.AddDefaultIdentity<ApplicationUser>()
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddClaimsPrincipalFactory<MyUserClaimsPrincipalFactory>();  // 追加
}
  • Viewでは@(User.FindFirst("ContactName").Value)!でアクセスできる.

TODO Razor Pages カスタマイズしたUserClaimを更新する

Razor Pages リテラル文字列の表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@foreach (var city in cities)
{
   if (city.Country == "UK")
   {
       @:Country:  @city.Country, Name: @city.Name
   }
}

@foreach (var city in cities)
{
   if (city.Country == "UK")
   {
       <text>Country:  @city.Country<br />
       Name: @city.Name</text>
   }
}

Razor Pages リモートバリデーション

Razor Pages リレーションがついたモデルの更新

Razor Pages ローカリゼーション

Razor Pages ロガー・ロギング

Razor Pages ロガーをテストでモックする

  • URL
  • 次のようにモックオブジェクトが作れる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
        var mock = new Mock<IStoreRepository>();
        mock.Setup(m => m.Products).Returns(new[]
        {
            new Product {ProductID = 1, Name = "P1"},
            new Product {ProductID = 2, Name = "P2"}
        }.AsQueryable());

        // これがロガーのモックオブジェクト
        var mockLogger = new Mock<ILogger<ListModel>>();

        var listModel = new ListModel(mock.Object, mockLogger.Object);
        listModel.OnGet(null,1);

Razor Pages ルーティング設定

  • 各ページで@page "/customer/order/{ShopId}/{Token?}"などを設定すればよい.
  • TODO: 一括設定できないだろうか?

URL末尾に常にスラッシュをつける

  • AppendTrailingSlash

Web API IActionFilter

  • 参考記事, 公式: ASP.NET Core Web API のエラーを処理する
  • HttpGetなどのHTTPメソッド属性でエラー ハンドラー アクション メソッドをマークしてはいけない.
    • 明示的な動詞を使うと要求がアクション メソッドに届かない可能性がある.
    • Swagger/OpenAPIを使うWeb APIの場合, エラー ハンドラー アクションを[ApiExplorerSettings]属性でマークし, そのIgnoreApiプロパティをtrueに設定する.

環境タグヘルパー

環境変数の取得

1
2
3
4
using Microsoft.Extensions.Configuration;
private readonly IConfiguration _configuration;

var bucketName = _configuration["ENV_VAR_NAME"];

公式のプロジェクトテンプレート置場

タグヘルパー

  • サーバー側のプロパティとブラウザーにレンダリングされるフォームコントロールの間に双方向のバインディングが作れる

タグヘルパー asp-for

1
2
3
<input asp-for="CityName" />

// => <input type="text" id="CityName" name="CityName" value="" />

タグヘルパー asp-format

タグヘルパー [Display(Name="Hoge Fuga")]

  • 表示名が変更できる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[BindProperty]
public string Name { get; set; }
[BindProperty]
[Display(Name = "Maximum Number Of Guests")]
public int MaxNumberOfGuests { get; set; }
[BindProperty]
[Display(Name ="Day Rate")]
public decimal DayRate { get; set; }
[BindProperty]
[Display(Name = "Smoking Permitted")]
public bool SmokingPermitted { get; set; }
[BindProperty]
[DataType(DataType.Date)]
[Display(Name ="Available From")]
public DateTime AvailableFrom { get; set; }

タグヘルパー select

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using Microsoft.AspNetCore.Mvc .Rendering

public class CreateModel : PageModel
{
    [BindProperty]
    [Display(Name = "City")]
    public int SelectedCity { get; set; }
    public SelectList Cities { get; set; }
    public string Message { get; set; }
    public void OnGet()
    {
        Cities = GetCityOptions();
    }

    public void OnPost()
    {
        Cities = GetCityOptions();
        if (ModelState.IsValid)
        {
            var city = GetCityOptions().First(o => o.Value == SelectedCity.ToString());
            Message = $"You selected {city.Text} with value of {SelectedCity}";
        }
    }

    private SelectList GetCityOptions()
    {
        var cities =  new List<City>
        {
            new City{ Id = 1, Name = "London"},
            new City{ Id = 2, Name = "Paris" },
            new City{ Id = 3, Name = "New York" },
            new City{ Id = 4, Name = "Rome" },
            new City{ Id = 5, Name = "Dublin" }
        };
        return new SelectList(cities, nameof(City.Id), nameof(City.Name));
    }

    private SelectList GetCityOptions2()
    {
       var cities =  new List<SelectListItem>
       {
           new SelectListItem{ Value = "1", Text = "London"},
           new SelectListItem{ Value = "2", Text = "Paris" },
           new SelectListItem{ Value = "3", Text = "New York", Selected = true },
           new SelectListItem{ Value = "4", Text = "Rome" },
           new SelectListItem{ Value = "5", Text = "Dublin" }
       };
       return new SelectList(cities);
    }
}


@if (Request.HasFormContentType)
{
    <p>You selected @Model.SelectedCity</p>
    <p>@Model.Message</p>
}
<form method="post">
    <div class="mb-3">
        <label class="form-label" asp-for="SelectedCity"></label>
        <select class="form-control" asp-for="SelectedCity" asp-items="Model.Cities"></select>
    </div>
    <button class="btn btn-primary">Submit</button>
</form>

タグヘルパー select, optGroup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public string CountryName { get; set; }
private SelectList GetCityOptions()
{
   var cities = new List<City>
   {
       new City{ Id = 1, Name = "Barcelona" , CountryName = "Spain" },
       new City{ Id = 2, Name = "Cadiz" , CountryName = "Spain" },
       new City{ Id = 3, Name = "London", CountryName = "United Kingdom" },
       new City{ Id = 4, Name = "Madrid" , CountryName = "Spain" },
       new City{ Id = 5, Name = "Rome", CountryName = "Italy" },
       new City{ Id = 6, Name = "Venice", CountryName = "Italy" },
       new City{ Id = 7, Name = "York" , CountryName = "United Kingdom" },
   };
   return new SelectList(cities, nameof(City.Id), nameof(City.Name), null, nameof(City.CountryName));
}

タグヘルパー ファイルアップロード

  • <form method="post" enctype="multipart/form-data">の指定が重要

タグヘルパー ファイルアップロード時の拡張子制限

テスト: HttpContextのモック

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[TestMethod]
public void TestValuesController()
{
    ValuesController controller = new ValuesController();
    controller.ControllerContext = new ControllerContext();
    controller.ControllerContext.HttpContext = new DefaultHttpContext();
    controller.ControllerContext.HttpContext.Request.Headers["device-id"] = "20317";
    var result = controller.Get();
    //the controller correctly receives the http header key value pair device-id:20317
}

テスト: データベース更新系

  • Microsoft.EntityFrameworkCore.InMemoryを導入してインメモリでテストする.
  • 2022 Test Driven Development C#が参考になる

テスト: 統合テスト

認可 [AllowAnonymous]

認可 AllowAnonymousToPage

認可 AuthorizeAttribute

  • Authorize属性をハンドラーメソッドに追加
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using CityBreaks.Models;
using CityBreaks.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace CityBreaks.Pages;

[Authorize] // 注意
public class IndexModel : PageModel
{
    private readonly ICityService _cityService;

    public IndexModel(ICityService cityService) =>
        _cityService = cityService;

    public List<City> Cities { get; set; }
    public async Task OnGetAsync() =>
        Cities = await _cityService.GetAllAsync();
}

認可 FallbackPolicy

認可 Program.csでの一元管理

  • AuthorizePage: 単一のページに認可を追加
  • AuthorizeFolder: 指定したフォルダ内のすべてのページに認可を追加
  • AuthorizeAreaFolder: 指定された領域内の指定されたフォルダー内のすべてのページに認可を追加
1
2
3
4
5
builder.Services.AddRazorPages(options => {
   options.Conventions.AuthorizeFolder("/CityManager");
   options.Conventions.AuthorizeFolder("/CountryManager");
   options.Conventions.AuthorizeFolder("/PropertyManager");
});

認可 Role, IdentityRole

  • ロールよりクレームが推奨されている
  • ロールは下位互換性のために残された概念
  • クレームをユーザーに一括で割り当てるためのメカニズムとしてはロールが役立つ場面がある
1
2
3
4
5
6
7
8
builder.Services.AddDefaultIdentity<CityBreaksUser>(options => {
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
})
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<CityBreaksContext>();

認可 クレーム

認可 ポリシー

1
2
3
4
5
6
7
8
builder.Services.AddAuthorization(options =>
{
   options.AddPolicy("AdminPolicy", policyBuilder => policyBuilder.RequireRole("Admin"));
});

builder.Services.AddRazorPages(options => {
   options.Conventions.AuthorizeFolder("/RolesManager", "AdminPolicy");
});

認可 リソースへの認可

バッチ処理: 別プロジェクトで対応

  • バッチ処理用のコンソールアプリケーションのプロジェクトを作ってそちらで対応する.
  • Cysharp/ConsoleAppFrameworkがおすすめ.
  • DIもできる.
  • Program.csのサンプルは次の通り.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
var builder = ConsoleApp.CreateBuilder(args);
builder.ConfigureServices((ctx, services) =>
{
    IConfiguration config = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", false, true)
        .AddJsonFile($"appsettings.{environment}.json", true)
        .AddEnvironmentVariables()
        .Build();
    // AWS上のデータベースにつなげるための設定でなくてもよい
    var awsSecret = Environment.GetEnvironmentVariable("APCLUSTER_SECRET");
    var configurationConnectionString = config.GetConnectionString("DefaultConnection");
    // これは開発環境とステージング・本番用に接続文字列を適切に設定するために別途用意したメソッド
    var connectionString = DbConnectionStringService.GetConnectionStringByLoadBalancer(awsSecret, configurationConnectionString).Result;

    services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(connectionString));

    // DIもできる: 適切な`using`を設定しよう
    services.AddScoped<IProductService, ProductService>();
});

var app = builder.Build();
app.AddAllCommandType();
app.Run();

ファイルからの設定読み込み

  • 環境変数からの取得
    • 開発時はProperties/launchSettings.jsonprofiles.environmentVariablesに指定するとよい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "profiles": {
    "MySolution": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7024;http://localhost:5110",
      "environmentVariables": {
        "APBUCKET_NAME": "apbucket-name",
        "APBUCKET_CLOUDFRONT": "apbucket-cloudfront",
        "ASPNETCORE_ENVIRONMENT": "Development",
        "APCLUSTER_SECRET": "{\"password\":\"pass\",\"dbname\":\"mydb\",\"port\":5432,\"host\":\"localhost\",\"username\":\"user\"}"
      }
    }
  }
}
  • appsettings.jsonからの取得例
1
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

メール送信 MailKit

リクエストへの容量制限

  • [RequestSizeLimit(1048576)]

リリース時の容量削減

  • <PublishTrimmed>true</PublishTrimmed>

ルート制約の非同期処理

ロガーなしでログを出力する

  • いろいろあってstaticなクラスからDIのロガーなしでログを出力したい.
    • 具体的にはAWS上での調査用にこの状況が出てきた.
  • Console.WriteLine()を使えばよい.

例外処理

Blazor

.NET6.NET8での違い

  • レンダーモードなど大変更が入った.
  • 公式: レンダーモード
  • 対話型のサーバーレンダリングを有効化し, その後に対話型のWebAssemblyレンダリングを有効にするには-int|--interactivityオプションをAutoに設定
1
dotnet new blazor -o BlazorApp -int Auto

Blazor WebAssembly: ソリューション生成, --hosted情報あり

  • --hostedをつけて実行するとClient, Server, Sharedの三プロジェクトが生成される.
  • クライアントがServerに組み込まれる模様. --hostedなしでクライアント単独で生成し, あとからAPIサーバープロジェクト・シェアライブラリプロジェクトを生成した方がよさそう?
1
2
dotnet new blazorwasm -o Blazor --pwa
dotnet new blazorwasm -o BlazorHosted --pwa --hosted

Blazor: @pageに変数を指定したいときは@attribute [Route(Constants.CounterRoute)]とする

1
2
- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]

Blazor: フロントに送る容量をおさえつつBlazor Serverを使う

  • 参考
  • Blazor Serverを使い, SignalRによる通信を潰す
    • 規定で初回のサーバーサイドレンダリングは入る
  • _Host.cshtmlの修正
    • _framework/blazor.server.jsを読み込むscriptタグを削除する
    • タグヘルパーcomponentの属性指定でrender-modeStaticに書き換える
  • Program.csの修正
    • services.AddServerSideBlazor();を削除
    • app.UseEndpoints内のendpoints.MapBlazorHub();を削除
      • 2023/01時点ではapp.MapBlazorHub();か?

Blazor: 本番ビルドと開発ビルド

  • perplexity.aiに質問した結果: (2023/9で未検証)
  • #ifプリプロセッサディレクティブでビルド構成に基づいてコードを条件付きで含めればよい
  • csprojファイルでビルド構成を定義する: Debug, Release
  • dotnet build--configurationオプションで構成の名前を設定する.
    • dotnet build --configuration Release

Blazor: 容量削減

  • URL
  • .csprojに追記
1
2
3
4
5
6
7
  <PropertyGroup>
    <!-- 略 -->

    <!-- Reduce output size -->
    <InvariantGlobalization>true</InvariantGlobalization>
    <WasmEmitSymbolMap>false</WasmEmitSymbolMap>
  </PropertyGroup>

Blazor wasm: AWSでのデプロイ

EF Core (Entity Framework Core)

公式ドキュメント

参考メモ

インストール

1
2
3
4
5
6
7
8
dotnet add package Microsoft.EntityFrameworkCore --version 6.0.15
dotnet add package Microsoft.EntityFrameworkCore.InMemory

dotnet new tool-manifest
dotnet tool install dotnet-ef --version 6.0.15

dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 6.0.15

C#の型とデータベースの型を合わせる

  • [Column(TypeName = "decimal(8, 2)")]などを使う.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace SportsStore.Models;

public class Product
{
    public long? ProductId { get; set; }

    [Required(ErrorMessage = "Please enter a product name")]
    public string Name { get; set; } = string.Empty;

    [Required(ErrorMessage = "Please enter a description")]
    public string Description { get; set; } = string.Empty;

    [Required]
    [Range(0.01, double.MaxValue,
        ErrorMessage = "Please enter a positive price")]
    [Column(TypeName = "decimal(8, 2)")]
    public decimal Price { get; set; }

    [Required(ErrorMessage = "Please specify a category")]
    public string Category { get; set; } = string.Empty;
}

HasQueryFilter 論理削除フラグを自動で判定して処理する

  • URL
  • IEntityTypeConfigurationを実装したクラスでHasQueryFilterを設定すればよい.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
namespace ConsoleApp1.Models
{
    public class ItemContext : DbContext
    {
        public DbSet<Item> Items { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Item>().HasQueryFilter(s => !s.IsDeleted);  // s.IsDeleted == false って書いてるのと一緒ですよん
        }
    }
}

エラー対応 Postgresql利用時にduplicate key value violates unique constraint "PK_Products"

  • connectionString = connectionString + ";Include Error Detail=true";でエラーの詳細を出力する
  • DETAIL: Key ("Id")=(60) already exists.のように出る場合, Postgresql上で次のSQLを発行する
1
2
SELECT MAX("Id") FROM "Products";
SELECT last_value FROM "Products_Id_seq";
  • これらがずれている場合は修正が必要.
1
2
SELECT SETVAL ('"Products_Id_seq"', (SELECT MAX("Id") FROM "Products"));
SELECT last_value FROM "Products_Id_seq";

大文字小文字の無視: SQLiteだけ?

  • EF.Functions.Collateを使う
1
2
3
4
5
6
7
8
public async Task<City> GetByNameAsync(string name)
{
    name = name.Replace("-"," ");
    return await _context.Cities
        .Include(c => c.Country)
        .Include(c => c.Properties.Where(p => p.AvailableFrom < DateTime.Now))
        .SingleOrDefaultAsync(c => EF.Functions.Collate(c.Name, "NOCASE") == name);
}

グローバルクエリフィルター

  • (要調査・実装)論理削除された項目を自動で見なくできる設定がある?

結合した子テーブルを適切な条件でフィルターする方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public async Task<Shop?> GetShopWithProductsByIdAsync(int id)
{
    return await _context.Shops
        .Join(_context.Products.Where(p => !p.IsDeleted),
            s => s.Id,
            p => p.ShopId,
            (s, p) => new {s, p})
        .GroupBy(x => x.s)
        .Select(x => new Shop
        {
            Id = x.Key.Id,
            Name = x.Key.Name,
            Products = x.Select(y => y.p).ToList()
        })
        .FirstOrDefaultAsync(x => x.Id == id);
}

更新時

  • いったん適当な手段でデータを取り, そのデータを更新する形で処理すること.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        var shop = await _shopService.GetByIdAsync(Id); // この取得部分が大事
        if (shop == null) return NotFound();
        shop.Name = Name;
        shop.Place = Place;
        shop.IsOpen = IsOpen;
        shop.MaitreDTimeZoneId = TimeZoneId;
        shop.DiscountAmount = DiscountAmount;
        shop.DiscountRate = DiscountRate;
        shop.VatRate = VatRate;
        shop.PaymentMethods = PaymentMethods.Select(p => new PaymentMethod
        {
            ShopId = Id,
            Name = p
        }).ToList();

        try
        {
            await _shopService.UpdateAsync(shop);
        }
        catch (DbUpdateConcurrencyException e)
        {
            _logger.LogError("ERROR: {E}", e.Message);
            RedirectToPage("/Error");
        }

シードでidGuidで発行する

  • URL
  • Guid.NewGuid()を使う
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void Configure(EntityTypeBuilder<Student> builder)
{
    builder.ToTable("Student");
    builder.Property(s => s.Age)
        .IsRequired(false);
    builder.Property(s => s.IsRegularStudent)
        .HasDefaultValue(true);

    builder.HasData
    (
        new Student
        {
            Id = Guid.NewGuid(),
            Name = "John Doe",
            Age = 30
        },
        new Student
        {
            Id = Guid.NewGuid(),
            Name = "Jane Doe",
            Age = 25
        },
        new Student
        {
            Id = Guid.NewGuid(),
            Name = "Mike Miles",
            Age = 28
        }
    );
}

シードを使う

  • 参考
  • Data/Configuration/HogeConfiguration.csIEntityTYpeConfigurationを実装したクラスを作る
  • Configureで適切に生成する
  • DbContext.csOnModelCreatingConfigurationを呼び出す
  • モデルでpublic ICollection<City> Cities { get; set; }などをnew()していると怒られる模様

SportsStoreの場合

  • Models/SeedData.csなどを適切に作る
  • Program.csで読み込む
1
2
using SportsStore.Models;
SeedData.EnsurePopulated(app);

シード: IdentityUser

シード: 多対多

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public Author Author { get; set; }
        public ICollection<BookCategory> BookCategories { get; set; }
    }
    public class Category
    {
        public int CategoryId { get; set; }
        public string CategoryName { get; set; }
        public ICollection<BookCategory> BookCategories { get; set; }
    }
    public class BookCategory
    {
        public int BookId { get; set; }
        public Book Book { get; set; }
        public int CategoryId { get; set; }
        public Category Category { get; set; }
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BookCategory>()
            .HasKey(bc => new { bc.BookId, bc.CategoryId });
        modelBuilder.Entity<BookCategory>()
            .HasOne(bc => bc.Book)
            .WithMany(b => b.BookCategories)
            .HasForeignKey(bc => bc.BookId);
        modelBuilder.Entity<BookCategory>()
            .HasOne(bc => bc.Category)
            .WithMany(c => c.BookCategories)
            .HasForeignKey(bc => bc.CategoryId);
    }

主キーが外部キーの場合のモデルの書き方

1
2
3
4
5
public class Shop
{
    public int Id { get; set; }
    public string Name { get; set; } = default!;
}
1
2
3
4
5
6
public class OrderNumber
{
    [Key] public int ShopId { get; set; }
    [ForeignKey("ShopId")] public Shop Shop { get; set; } = default!;
    public int Number { get; set; }
}

データベースのリセット・削除

  • cf. 検索用: delete, destroy
1
dotnet ef database drop --force --context <データベースコンテキスト>

認証

  • ASP.NET Core Identityを使う.

認証用ユーザーにリレーションを張る

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Microsoft.AspNetCore.Identity;

namespace <ProjectName>.Models;

public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; } = string.Empty;

    public string LastName { get; set; } = string.Empty;

    public DateTime BirthDate { get; set; }

    public virtual IEnumerable<Article>? Articles { get; set; }
}


namespace <ProjectName>.Models;

public class Article
{
    public int ArticleId { get; set; }

    public string Title { get; set; } = string.Empty;

    public string Description { get; set; } = string.Empty;

    public string? UserId { get; set; }

    public virtual ApplicationUser? User { get; set; }
}

認証用ユーザーの主キーを変える

  • URL
  • 未検証

マイグレーションの作成

  • ローカルインストールしたEF Coreを使う場合は次の通り.
1
2
dotnet dotnet-ef migrations add InitialCreate
dotnet dotnet-ef migrations add Rating
  • コンテキストを指定したい場合は次の通り.
1
dotnet dotnet-ef migrations add InitialCreate -c <データベースコンテキスト>
  • EF Coreのツールをグローバルインストールしている場合は下記コマンド.
1
dotnet ef migrations add InitialCreate

マイグレーションの自動適用

  • 2021 Entity Framework Core in Action, P.149
  • 参考: Program.csConfigureメソッド内でMigrationsを適用する.
1
2
3
4
5
using (var context = new SchoolContext(
    services.GetRequiredService<DbContextOptions<SchoolContext>>()))
{
    context.Database.Migrate();
}

マイグレーションの反映

1
dotnet dotnet-ef database update

マイグレーションのundo

1
dotnet ef migrations remove

リバースエンジニアリング・データベースファースト・スキャフォールド

  • データベース側の準備一例
    • A5SQLを使って作ったER図doc/mydba5erからSQLを生成してdb/init/init.sqlに置く
    • SQLでIDIdに置換する
    • データベースを初期化:dockerを立ち上げれば自動的に初期化される
      • 再作成したい場合はdocker compose downしてからdocker compose up --build
  • TODO:接続文字列のセキュリティに関連してシークレットマネージャーツールを使うべしと怒られる
  • cf. スキャフォールドのオプション
  • 公式
  • 接続文字列・プロバイダーは適宜調べよう
    • SQLite: Microsoft.EntityFrameworkCore.Sqlite
    • PgSQL: Npgsql.EntityFrameworkCore.PostgreSQL
    • MySQL:MySql.EntityFrameworkCore`
    • MariaDB: Pomelo.EntityFrameworkCore.MySql
    • SQL Server: Microsoft.EntityFrameworkCore.SqlServer
1
2
3
4
5
dotnet dotnet-ef dbcontext schema <接続文字列> <Provider>


dotnet dotnet-ef dbcontext scaffold "Data Source=RazorPages.Data.db" Microsoft.EntityFrameworkCore.Sqlite
dotnet dotnet-ef dbcontext scaffold 'Host=localhost;Database=mydb;Username=user;Password=pass' Npgsql.EntityFrameworkCore.PostgreSQL --output-dir Models --context-dir Context --context MyDbContext

リレーションの設定

リレーションの読み込み: Eager load

  • 2021 Entity Framework Core in Action, P77
  • Include, ThenIncludeを使う
1
2
3
var firstBook = await _context.Books()
    .Include(b => b.Reviews)
    .FirstOrDefaultAsync();

リレーションの読み込み: 明示的な読み込み

  • 2021 Entity Framework Core in Action, P79
1
2
3
var firstBook = _context.Books.First();
_context.Entry(firstBook)
    .Collection(b => b.AuthorsLink).Load();

リレーションの読み込み: Select

  • 2021 Entity Framework Core in Action, P80
1
2
3
4
5
var books = _context.Books.Select(b => new {
    b.Title,
    b.Price,
    NumReviews = b.Reviews.Count
}).ToList();

リレーションのメモ

  • AsSplitQuery()
  • AsNoTracking()

C

APIドキュメントのJSONからC#のクラス定義を作る

Delegate

  • .NETのデリゲートはメソッドシグネチャと戻り値の型を表す型.
  • 次の例ではMyDelegateという名前のデリゲートを宣言
    • DateTimeをパラメータとして整数を返す
1
delegate int MyDelegate(DateTime dt);
  • 一致するシグネチャと戻り値の型に基づいてデリゲートにメソッドを割り当てる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int GetMonth(DateTime dt)
{
    return dt.Month;
}
int PointlessAddition(DateTime dt)
{
    return dt.Year + dt.Month + dt.Day;
}

MyDelegate example1 = GetMonth;
MyDelegate example2 = PointlessAddition;
Console.WriteLine(example1(DateTime.Now));
Console.WriteLine(example2(DateTime.Now));
  • デリゲートにインライン匿名メソッドを割り当てる
1
2
3
4
5
MyDelegate example3 = delegate(DateTime dt) { return dt.Now.AddYears(-100).Year; };
Console.WriteLine(example3(DateTime.Now));

MyDelegate example4 = (dt) => { return dt.Now.AddYears(-100).Year; };
Console.WriteLine(example4(DateTime.Now));

JsonSerializerUTF-8エンコードを外して日本語を正しく表示させる

1
2
3
var json = JsonSerializer.Serialize(creditVerifyCardRequest);
_testOutputHelper.WriteLine("👺 リクエストのJSON(日本語文字化け)");
_testOutputHelper.WriteLine(json);
  • 次のように書くとよい.
1
2
3
4
5
6
7
var options = new JsonSerializerOptions
{
    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var json2 = JsonSerializer.Serialize(creditVerifyCardRequest, options);
_testOutputHelper.WriteLine("👺 リクエストのJSON2(日本語文字化けしない)");
_testOutputHelper.WriteLine(json2);

GUID

1
2
// System.Guid.NewGuid()
System.Console.WriteLine(Guid.NewGuid());

SHIFT_JISまたはWINDOWS-31Jを扱う

.NET CoreはASCIIUTF-8しか使えない. 次のようなコードを書くとSHIFT_JIS文字列が扱える. 少なくとも英数だけならWINDOWS-31JSHIFT_JIS扱いでよいようだ.

1
2
3
4
5
using var httpClient = new HttpClient();
var formContent = new FormUrlEncodedContent(param);
formContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
formContent.Headers.ContentEncoding.Add("windows-31j");
using var response = await _sut.CallApiAsync(urlString, param);
1
2
3
var shiftJis = Encoding.GetEncoding("shift_jis");
var content = shiftJis.GetString(await response.Content.ReadAsByteArrayAsync());
var resultDictionary = _sut.ShiftJisByteArrayToDictionary(await response.Content.ReadAsByteArrayAsync());

UTCを適切なタイムゾーンに変換する

1
2
3
4
var utcDateTime = DateTime.UtcNow;
TimeZoneInfo tokyoTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
DateTime convertedDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, tokyoTimeZone);
Console.WriteLine(convertedDateTime);

イミュータブルコレクション

インターフェースに対するNotImplementedException

  • 実装しないメソッドに対してthrow new NotImplementedException();をつける
  • インターフェースに標準実装をつけておいてもよい

インターフェースの利用・階層

2023-01-23, .NET6

環境変数を取得する

1
2
3
var envRegion = Environment.GetEnvironmentVariable("REGION");
var envCognitoUserPoolId = Environment.GetEnvironmentVariable("COGNITO_USER_POOL_ID");
var envClientId = Environment.GetEnvironmentVariable("CLIENT_ID");

警告をエラー扱いにするcsprojの設定

1
2
3
4
5
6
7
8
<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <!-- 次の設定を`true`にする -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

コードアナライザー

  • URL
  • csprojまたはDirectory.Build.props<EnableNETAnalyzers>true</EnableNETAnalyzers>を追加する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <AnalysisLevel>Recommended</AnalysisLevel>
  </PropertyGroup>

</Project>

ソリューション全体への設定

  • URL
  • Directory.Build.props
    • csprojの"前"に読み込まれる。
    • csprojで上書きができる
  • Directory.Build.target
    • csprojの"後"に読み込まれる。
    • csprojの設定を上書きできる

タイムゾーンIDの取得

1
TimeZoneInfo.GetSystemTimeZones().ToList().ForEach(t => Console.WriteLine(t.Id))

テストコードの命名規則

  • Method_Condition_Expectation
  • Method_Should_When

ドメイン固有の概念実装

  • URL
  • 書籍MetaProgramming in C#参照.
  • 次のようなレコードを作る
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace Fundamentals;
public record ConceptAs<T>
{
    public ConceptAs(T value)
    {
        ArgumentNullException.ThrowIfNull(value,
          nameof(value));
        Value = value;
    }
    public T Value { get; init; }
    public static implicit operator T(ConceptAs<T> value)
      => value.Value;
}
  • 特定のレコードを作成
1
2
3
public record FirstName(string Value) : ConceptAs<string>(Value);
public record LastName(string Value) : ConceptAs<string>(Value);
public record SocialSecurityName(string Value) : ConceptAs<string>(Value);
  • これらを元にPersonクラスを生成
1
2
3
4
5
6
7
namespace Domain.Employees;
public class RegisterEmployee
{
    public FirstName FirstName { get; set; } = new(string.Empty)
    public LastName LastName { get; set; } = new(string.Empty);
    public SocialSecurityNumber SocialSecurityNumber { get; set; } = new(string.Empty);
}

配列の初期化

  • 整数0で初期化するなら次のように書けば十分.
1
int[] table = new int[100];
  • Enumerable.Repeatもある.
1
2
// 100個の要素を-1で初期化
int[] table = Enumerable.Repeat<int>(-1, 100).ToArray();
  • 一般
1
bool[,] dp = new bool[n+1, m+1];

プロジェクト構成

  • Apps and Services with .NET 7から
  • Northwind.Common
    • 複数のプロジェクトで使われるインターフェイス・列挙型・クラス・レコード・構造体など, 一般的な型のクラス ライブラリ プロジェクト。
  • Northwind.Common.EntityModels
    • 一般的な EF Core エンティティ モデルのクラス ライブラリ プロジェクト。 エンティティ モデルはサーバー側とクライアント側の両方で使用されることが多いため、 特定のデータベース プロバイダーへの依存関係を分離するとよい.
  • Northwind.Common.DataContext
    • 特定のデータベース プロバイダーに依存する EF Core データベース コンテキストのクラス ライブラリ プロジェクト.
  • Northwind.Mvc
    • MVC パターンを使って簡単に単体テストできる複雑な Web サイト用の ASP.NET Core プロジェクト.
  • Northwind.WebApi.Service
    • HTTP API サービス用の ASP.NET Core プロジェクト. 任意の JavaScript ライブラリまたは Blazor を使用してサービスと対話できるため, Web サイトとの統合によい.
  • Northwind.WebApi.Client.Console
    • Web サービスのクライアントで特にコンソールアプリ.
  • Northwind.gRPC.Service
    • gRPC サービス用の ASP.NET Core プロジェクト.
  • Northwind.gRPC.Client.Mvc
    • gRPC サービスのクライアント。 名前の最後の部分は、ASP.NET Core MVC Web サイト プロジェクト.
  • Northwind.BlazorWasm.Client
    • ASP.NET Core Blazor WebAssembly のクライアント側プロジェクト.
  • Northwind.BlazorWasm.Server
    • ASP.NET Core Blazor WebAssembly サーバー側プロジェクト.
  • Northwind.BlazorWasm.Shared
    • クライアント側とサーバー側の Blazor プロジェクト間で共有されるクラス ライブラリ.

AWS

Lambda上でのASP.NET Core

記事メモ

F

.envファイルを読み込みたい

  • URL
  • .envと書いたがiniファイルなど同じように読める形式はもう少しあるはず.
  • シンプルは読み込み関数はない(?)らしい.
  • 次のコードで環境変数に叩き込めるから後は環境変数読み込みの形で取ればよい.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
open System
open System.IO

let parseLine(line : string) =
  Console.WriteLine (sprintf "Parsing: %s" line)
  match line.Split('=', StringSplitOptions.RemoveEmptyEntries) with
  | args when args.Length = 2 -> Environment.SetEnvironmentVariable(args.[0], args.[1])
  | _ -> ()

let load() =
  lazy (
    Console.WriteLine "Trying to load .env file..."
    let currentDir = Directory.GetCurrentDirectory()
    let pDir = Directory.GetParent(currentDir).ToString() // 都合によって親ディレクトリの.envを読んでいるが必要なければ削除
    let filePath = Path.Combine(pDir, ".env") // 親ディレクトリの.envを読んでいるので同じディレクトリにあるなら適切に書き換える
    filePath
    |> File.Exists
    |> function
      | false -> Console.WriteLine "No .env file found."
      | true  -> filePath |> File.ReadAllLines |> Seq.iter parseLine
  )
let init = load().Value

// 環境変数から読み込む部分
let apiRootUrl = Environment.GetEnvironmentVariable "NEXT_PUBLIC_API_ROOT_URL"

bottom

1
2
3
4
let undefined<'T> : 'T = failwith "Not implemented yet"

let stub1 (x : int) : float = undefined
let stub2 (x : 'T) : 'T = undefined
1
let undefined<'T> : 'T = raise (NotImplementedException())

fsx実行: 競プロ用実行

1
2
dotnet fsi hoge.fsx
dotnet fsi hoge.fsx < input.txt

fsxで他のfsxを読み込む

1
2
#load "Script1.fsx"
open Script1

Fable

メモ

1
2
dotnet new --install Fable.Template
dotnet new fable

Json

  • 参考
  • C#のコードを参考にすればよい
  • mutable<-を使うのが大事
1
2
3
4
5
6
7
8
9
open System.Text.Json
open System.Text.Json.Serialization
open System.Text.Encodings.Web
open System.Text.Unicode

let mutable options = JsonSerializerOptions()
options.Encoder <- JavaScriptEncoder.Create(UnicodeRanges.All)
let jsonString = JsonSerializer.Serialize(items, options)
System.IO.File.WriteAllText(outputJsonFileName, jsonString, System.Text.Encoding.UTF8)

Stack overflow

AtCoderの木DPで, Pythonコードに沿って実装してみたらStack overflowしたのでその対処の記録.

  • 再帰関数で stack overflow しないように末尾再帰に書き直すのはF#だけでなく関数プログラミングで必須技能なので練習しよう
  • 再帰の回数の上限ではなくスタックに割り当てられるメモリの量に制限がある
  • 管理は.NETではなくOS
  • Linuxならulimit -sで変更できるはず
  • 参考: Python版

Stringにはreverseがない(?)ので代替策を見つけた

どなたか有識者の方, いい感じのメソッドがあれば教えてほしい. 今回はProject Euler Problem 4の回文数の問題を解いているときに出くわした. どうもStringにはreverseメソッドがないらしい. 検索した結果, 今回はこのページのコピペとして次のコードを採用した.

1
2
let reverse (s : string) = s |> Seq.toArray |> Array.rev |> System.String
s |> Seq.rev |> System.String.Concat

あと最近F#のリファレンスがGitHubにうつったようだ. そのリンクも改めて貼っておこう.

GitHubのAlgorithmsAndDataStructureByFSharpにアルゴリズム系のコードと共にF#の「ライブラリ」という名の自分用コードサンプル集を作ってある. 具体的にはLibraryディレクトリがそう. そのうち競プロ用にいろいろ調べたことを改めてLibraryにまとめ直したい. 何はともあれ,

F#, とにかく情報がないので地道に貯めていく.

型プロバイダー FSharp.Dataの型プロバイダーには定数しか渡せない

  • URL
  • 下記コードでFSharp.Data.JsonProvider<"http://localhost"><>内は変数にできない
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#r "nuget: FSharp.Data"
open FSharp.Data
open FSharp.Data.HttpRequestHeaders

let toJson = fun (x: HttpResponse) ->
  match x.Body with
  | Text t -> t
  | _ -> failwith "bytes"

/// http://localhost
type Root = FSharp.Data.JsonProvider<"http://localhost">
Http.Request("http://localhost", httpMethod = "GET") |> toJson |> fun t -> Root.Parse(t)

型プロバイダー 実行時のディレクトリ指定

  • URL
  • ResolutionFolder=__SOURCE_DIRECTORY__を指定する
1
2
3
4
#r "nuget: FSharp.Data"
open FSharp.Data

type Comments = JsonProvider<"sampleResponse.json", ResolutionFolder=__SOURCE_DIRECTORY__>;;

関数に型をつける

ふつうの方法

1
let f (i:int) (j:int): int = i * j

ラムダを使う方法

1
2
let f: int -> int -> int =
  fun i j -> i * j

空シーケンスの判定, 特にmatch

  • 空シーケンスのリテラルがないのでs when Seq.isEmpty sのように書く.
1
2
3
4
let rec dropWhile p xs =
  match xs with
    | s when Seq.isEmpty s -> Seq.empty
    | _ -> if p (Seq.head) then dropWhile p (Seq.tail xs) else (Seq.tail xs)

処理時間測定

1
2
3
4
5
6
open System.Diagnostics
let sw = System.Diagnostics.Stopwatch()
sw.Start()
// 処理
sw.Stop()
stdout.WriteLine(sw.Elapsed)

遅延リストはとりあえずSeq

  • 残念ながらHaskellのリストなどと同じ書き味にはならない
1
Seq.initInfinite (fun i -> 2 * i + 1)

ファイルの読み書き

  • C# (.NET)のメソッドを使おう
  • JSONの場合の注意
    • 既定のエンコーダーと比較してUnsafeRelaxedJsonEscapingエンコーダーは文字をエスケープせずにそのまま渡すことについてより寛容
    • <, >, &, 'などHTMLに影響する文字はエスケープされない
    • s.Replace(@"\u002B", "+").Replace(@"\u003C", "<").Replace(@"\u0027", "'").Replace(@"\u0026", "&").Replace(@"\u003E", ">")で置換する
1
2
let fileName = "1.tmp.txt"
System.IO.File.WriteAllText(fileName, someString, System.Text.Encoding.Default)

複数行の文字列, ヒアドキュメント

C#と同じように書ける模様. 特に三重引用符または「@+クオート」で書ける. ついでに逐次的リテラル文字列の概念があり, 後者の「@+クォート」形式がそれ. 例を引用しておく.

1
2
".¥¥hoge¥¥hoge¥¥hoge.txt" // ふつうの文字列
@".¥hoge¥hoge¥hoge.txt"   // 逐次的リテラル文字列

プロジェクト作成

VSCodeを前提にする.

  • 下記の内容のファイルをsetup.txtとして作る.
    • MySolutionMyProjectは適宜修正.
    • Ctrl+F2で一斉置換できる.
  • Ctrl+Shift+Pでコマンドパレットを開いて`TRSTAT と打って実行.

setup.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
dotnet new sln -o MySolution
cd MySolution
mkdir src
dotnet new console -lang F# -o src/MyProject
dotnet sln add src/MyProject/MyProject.fsproj
mkdir tests
dotnet new xunit -lang F# -o tests/MyProjectTests
dotnet sln add tests/MyProjectTests/MyProjectTests.fsproj
cd tests/MyProjectTests
dotnet add reference ../../src/MyProject/MyProject.fsproj
dotnet add package FsUnit
dotnet add package FsUnit.XUnit
dotnet build
dotnet test