.NET
お役立ちライブラリ
- 使い方は
ChatGPT
で聞いてみるのもよい. - Polly:
HTTP
リクエストを適当な感覚で自動で複数回飛ばしてくれる - Bogus: フェイクデータ(モックデータ)の生成
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.
と言われてしまう
- マルチステージビルド用の
Dockerfile
はRider
による生成 - 最初に結論: 「
base
のイメージを適切に選ぶ」 - 実際に起きたのは次のような状況
Main
プロジェクト: ASP.NET Core Batch
プロジェクト: バッチジョブ用のコンソールアプリケーション Batch
でMain
のファイルを参照している 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キャッシュの削除
| dotnet nuget locals --clear all
|
インストールされているバージョンを調べる
ローカルツールのインストール手順
| dotnet new tool-manifest
dotnet tool install ef
|
コマンドサンプル
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
でバージョンを指定する.
ソリューション初期化
| dotnet new sln # 現在のディレクトリに同名のファイル生成
dotnet new sln --name <MySolution> # 現在のディレクトリにファイル生成
dotnet new sln --output <MySolutionDirectory> # 指定したディレクトリに生成
|
ソリューションへのプロジェクト追加
| dotnet sln add <HogeProject>
|
使えるテンプレートのリスト
| dotnet new list
dotnet new search HOGEHOGE
|
- 上のリストから適当に対象を見つける
- 例えば「短い名前」欄の値を使って次のように新規プロジェクトが作れる
| dotnet new react --auth Individual -o MyReactApp
|
バージョンを削除する: Mac
またはLinux
の場合
dotnet --list-sdks
でディレクトリを調べる - 該当ディレクトリを削除
バージョンを指定する
| dotnet new globaljson
dotnet --list-sdks
|
バージョンを調べる
パッケージを追加する
| dotnet add package <hoge> --version
|
パッケージを削除する
| dotnet remove package <hoge>
|
プロジェクト初期化時の参考
| 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>
|
プロジェクトの参照追加
Emacs連携 FSAutoCompleteが導入できないとき
- F#のファイルを開いたときに次のようなメッセージが出るとき
コマンドが誤っています
dotnet-path/to/.emacs.d/.cache/lsp/fsautocomplete/fsautocomplete.dllが見つかりません.
/tmp
や~/AppData/Local/Temp
にfsautocomplete1HbFmC.zip
のようなファイルがダウンロードできていたので, これを~/.emacs.d/.cache/lsp
に展開. - もう一度F#のファイルを開いてLSPが発動するか確認
Emacs連携 FsAutoComplete
- LSPからの自動インストールによく失敗する.
- 直接公式からクローンしてビルドする.
- 直下の
global.json
とdotnet
のバージョンを合わせること. dotnet tool restore
でfake
を入れてからdotnet fake build
. src/FsAutoComplete/bin/Release/net5.0
にビルドされている - これを
~/.emacs.d/.cache/lsp/fsautocomplete
にコピー
EmacsでのFSACエラー: Macでの調整
| 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 に置かれる?
| 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連携: ツールチップなどが出てこない
vscode連携: やたらエラーが出る
- 参考
- エラーを出したディレクトリに
.nuget/packages/
へのシンボリックリンクを張れば良い(?) - 自分用参考: コマンドプロンプトで
mklink myhome\junk\fsharp\packages myhome\.nuget\packages
myhome
ホームディレクトリへのフルパス: 使うマシンごとに切り替えよう. - Windows のコマンドプロンプトだから, ディレクトリの区切りは
\
で.
TODO Windowsでの英語化
- 参考: URL
- Visual Studio Installerで適当に設定を眺めてみる
アップデート
- 2022/12時点では公式からバイナリインストールするしかない模様.
アンインストール: Mac
/tmp直下
事前にバージョン(curlでのダウンロード先)を確認すること.
| 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でのダウンロード先)を確認すること.
| 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
インストール: 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
などが標準.
| 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.json
とappSetting.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章
2章
| dotnet tool install -g LiveReloadServer
|
- P.39, OpenAPIサポートのためにNuGetの
Swashbuckle.AspNetCore
がある
| 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を使う
| dotnet dev-certs https --clean
dotnet dev-certs https --trust
|
ローカルでProduction
を実行
| dotnet run --environment Production
|
開発環境での環境変数の設定
- URL
Properties/launchSettings.json
で設定すればよい. - 特にAWSでの公開を前提にするならAWSでは環境変数に接続文字列の情報が格納されるため, はじめから同じ形式で環境変数を設定しておくとよい.
環境変数から値を読み込む方法
環境変数を設定する
| dotnet run --environment Production
|
シークレットをPOCO
に設定する
| {
"Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
"Movies:ServiceApiKey": "12345"
}
|
| var moviesConfig =
Configuration.GetSection("Movies").Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;
|
| public class MovieSettings
{
public string ConnectionString { get; set; }
public string ServiceApiKey { get; set; }
}
|
初期化
| dotnet tool restore
dotnet restore
|
短縮URL作成
導入メモ: global.json
| {
"sdk": {
"version": "6.0.400"
}
}
|
導入メモ: docker-compose.yml
- バージョンは都度修正すること.
.env
は開発サンプル用設定として共通にしている: 必要に応じて適切な値を設定すること.
| 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
| 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 Core
はdotnet dotnet-ef
コマンドで呼び出す - バージョンは適宜最新化すること
| dotnet new tool-manifest
dotnet tool install dotnet-ef --version 6.0.14
|
EF Core
をグローバルインストールしていて, グローバルのツールのバージョンを合わせたい場合は次のコマンドを発行する.
| dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 6.0.14
|
導入メモ: パッケージのインストール
| dotnet add package Microsoft.EntityFrameworkCore --version 6.0.14
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 6.0.13
|
| 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で必要
|
| dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design -v 6.0.11
|
導入メモ: Program.cs
への各データベース設定
SQLite
| builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.")));
|
In Memory
| builder.Services.AddDbContext<MyDbContext>(opt =>
opt.UseInMemoryDatabase("mydb"));
|
PostgreSQL
| builder.Services.AddDbContext<MyDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("MyDbContext") ??
throw new InvalidOperationException("Connection string 'MyDbContext' not found.")));
|
導入メモ: appsettings.json
への各データベース設定
SQLite
| "ConnectionStrings": {
"DefaultConnection": "DataSource=Migrations/app.db;Cache=Shared"
},
|
SQLiteメモリ
| "ConnectionStrings": {
"DefaultConnection": "DataSource=:memory:"
},
|
PostgreSQL
| "ConnectionStrings": {
"DefaultConnection": "User ID=user;Password=pass;Host=localhost;Port=5432;Database=mydb;"
},
|
導入メモ: フロントエンド用ライブラリ導入, LibMan
でのインストール
- MacでLibManを使うときは
export PATH="$PATH:/Users/username/.dotnet/tools"
を設定すること. - これでうまくいかない場合は次の通り.
| dotnet new tool-manifest
dotnet tool install Microsoft.Web.LibraryManager.Cli
|
dotnet libman
で実行 LibMan (Microsoft.Web.LibraryManager.Cli)
は(ローカル)インストール済みとする. wwwroot/lib
を削除する.
| 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
を発行している - 具体的には次のようなコマンドを発行する
| 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
のマイグレーション
導入メモ: テストプロジェクトを作る
| 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
|
| 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の設定を変える
| 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.cs
のpublic 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
- 必要に応じてバージョン指定でインストールしよう.
| 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
|
| dotnet aspnet-codegenerator identity -h
|
- あとは
Rider
またはVisual Studio
でスキャフォールドするとよい. Visual Studio
での参考URL
Identity: パスワードへの制約
Program.cs
でbuilder.Services.AddDatabaseDeveloperPageExceptionFilter();
の下あたりに次の設定を追加する
| 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: パスワードハッシュの値を固定する
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を取得する
| var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
Identity: ユーザーのキーの型をint
に変更
| public class ApplicationUser : IdentityUser<int>
{
}
|
- なくてもいいがついでに
ApplicationRole
も作る
| 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
の初期値が入らなかった.
| 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でインストールしたツールをリストア
Minimal API: ログを仕込む
| 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();
}
}
|
| <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
ページのレイアウト」参照 RCL
でRazor Pages
を使う場合はホスティングアプリでRazor Pages
サービスとエンドポイントを有効化する
| 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}"
"{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
: 最小値を持つ整数に一致 max
: 最大値を持つ整数に一致 minlength
: 最短の文字列に一致 maxlength
: 最長の文字列に一致 - cf.
{postcode:maxlength(8)}
range
: 値の範囲内の整数に一致 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
を指定すると, その値に対してハンドラーOnPostHoge
やOnGetFuga
を呼び出せる. - 特にクエリ文字列に
?handler=Hoge
などを追加する.
Razor Pages AutoMapper
- 公式
- 大量のモデルバイディングで便利なライブラリ
- 2022年時点では
ASP.NET Core Razor Pages in Action 2nd edition
で推奨
Razor Pages HTMLエンコードしたくない場合
| @{
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
を使うのも一手.
| builder.Services.Configure<WebEncoderOptions>(options =>
{
options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Latin1Supplement);
});
|
Razor Pages POSTでリクエストが飛ばない
- 次の可能性を検討する
form
タグのaction
属性またはmethod
属性の指定. - フォームフィールドの
name
属性の設定 - モデルのバリデーションに失敗している.
OnPost
のハンドラーが存在しているか. - フォームに必要なAntiforgeryトークンがない. トークン設定は次の通り.
| <div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
Razor Pages removeTagHelper
ディレクティブ: 特定のタグの処理を選択的にオプトアウト
| @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
キーと値のペアとして, 大文字と小文字を区別しない文字列キーを参照してアクセスする
| 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-handler
とOnPostHoge
・OnPostFuga
などを使う
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
の三つのプロジェクトができる. - 特に
Server
はRazor Pages
になっている
| dotnet new blazorwasm -ho --auth Individual --pwa -o <SolutionName>
|
Server
でdotnet 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 タグヘルパーでのセレクトリストの生成
| @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;
}
}
|
| 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
を書き換えてフレームワーク側が出すログレベルを変える.
| {
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}
|
Razor Pages 部分ビュー
@page
ディレクティブがないファイル - ふつうのビューと違って
_ViewStart.cshtml
を呼び出さず, _Layout.cshtml
のレイアウトは適用されない - 規則ではないが, たいてい
_myPartial.cshtml
のようにファイル名の先頭にアンダースコアを付ける <partial name=”_NavigationPartial” />
で呼び出す - 親ビューから部分ビューに
ViewData
を渡す場合はview-data
属性にViewData
名を指定する - モデルを渡す場合は
for
属性または{model}属性に渡したいモデル名を指定する.
| <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
はそれぞれIdentityUser
とIdentityRole
の拡張.
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;
}
}
|
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;
}
}
|
| 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 ロガーをテストでモックする
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末尾に常にスラッシュをつける
Web API IActionFilter
- 参考記事, 公式: ASP.NET Core Web API のエラーを処理する
HttpGet
などのHTTP
メソッド属性でエラー ハンドラー アクション メソッドをマークしてはいけない. - 明示的な動詞を使うと要求がアクション メソッドに届かない可能性がある.
Swagger/OpenAPI
を使うWeb API
の場合, エラー ハンドラー アクションを[ApiExplorerSettings]
属性でマークし, そのIgnoreApi
プロパティをtrue
に設定する.
環境タグヘルパー
環境変数の取得
| using Microsoft.Extensions.Configuration;
private readonly IConfiguration _configuration;
var bucketName = _configuration["ENV_VAR_NAME"];
|
公式のプロジェクトテンプレート置場
タグヘルパー
- サーバー側のプロパティとブラウザーにレンダリングされるフォームコントロールの間に双方向のバインディングが作れる
タグヘルパー asp-for
| <input asp-for="CityName" />
// => <input type="text" id="CityName" name="CityName" value="" />
|
タグヘルパー [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
のモック
| [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
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
: 指定された領域内の指定されたフォルダー内のすべてのページに認可を追加
| builder.Services.AddRazorPages(options => {
options.Conventions.AuthorizeFolder("/CityManager");
options.Conventions.AuthorizeFolder("/CountryManager");
options.Conventions.AuthorizeFolder("/PropertyManager");
});
|
認可 Role
, IdentityRole
- ロールよりクレームが推奨されている
- ロールは下位互換性のために残された概念
- クレームをユーザーに一括で割り当てるためのメカニズムとしてはロールが役立つ場面がある
| 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>();
|
認可 クレーム
認可 ポリシー
| builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminPolicy", policyBuilder => policyBuilder.RequireRole("Admin"));
});
builder.Services.AddRazorPages(options => {
options.Conventions.AuthorizeFolder("/RolesManager", "AdminPolicy");
});
|
認可 リソースへの認可
バッチ処理: 別プロジェクトで対応
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.json
のprofiles.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\"}"
}
}
}
}
|
| var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
メール送信 MailKit
リクエストへの容量制限
[RequestSizeLimit(1048576)]
リリース時の容量削減
<PublishTrimmed>true</PublishTrimmed>
ルート制約の非同期処理
ロガーなしでログを出力する
- いろいろあって
static
なクラスからDI
のロガーなしでログを出力したい. - 具体的には
AWS
上での調査用にこの状況が出てきた.
Console.WriteLine()
を使えばよい.
例外処理
Blazor
.NET6
と.NET8
での違い
2024-05_C#ユーザーのためのWebアプリ開発パターン-ASP.NET_Core-Blazorによるエンタープライズアプリ開発.pdf
を見るとよい - レンダーモードなど大変更が入った.
- 公式: レンダーモード
- 対話型のサーバーレンダリングを有効化し, その後に対話型の
WebAssembly
レンダリングを有効にするには-int|--interactivity
オプションをAuto
に設定
| dotnet new blazor -o BlazorApp -int Auto
|
.NET8
でのいわゆるBlazor Server
Blazor WebAssembly
: ソリューション生成, --hosted
情報あり
--hosted
をつけて実行するとClient
, Server
, Shared
の三プロジェクトが生成される. - クライアントが
Server
に組み込まれる模様. --hosted
なしでクライアント単独で生成し, あとからAPIサーバープロジェクト・シェアライブラリプロジェクトを生成した方がよさそう?
| dotnet new blazorwasm -o Blazor --pwa
dotnet new blazorwasm -o BlazorHosted --pwa --hosted
|
Blazor
: @page
に変数を指定したいときは@attribute [Route(Constants.CounterRoute)]
とする
| - @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]
|
Blazor: フロントに送る容量をおさえつつBlazor Server
を使う
- 参考
Blazor Server
を使い, SignalR
による通信を潰す _Host.cshtml
の修正 _framework/blazor.server.js
を読み込むscript
タグを削除する - タグヘルパー
component
の属性指定でrender-mode
をStatic
に書き換える
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: 容量削減
| <PropertyGroup>
<!-- 略 -->
<!-- Reduce output size -->
<InvariantGlobalization>true</InvariantGlobalization>
<WasmEmitSymbolMap>false</WasmEmitSymbolMap>
</PropertyGroup>
|
Blazor wasm: AWSでのデプロイ
Database
調査: 2024-07-26
EF Core (Entity Framework Core)
公式ドキュメント
参考メモ
インストール
| 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
を発行する
| SELECT MAX("Id") FROM "Products";
SELECT last_value FROM "Products_Id_seq";
|
| SELECT SETVAL ('"Products_Id_seq"', (SELECT MAX("Id") FROM "Products"));
SELECT last_value FROM "Products_Id_seq";
|
大文字小文字の無視: SQLite
だけ?
| 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");
}
|
シードでid
をGuid
で発行する
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.cs
にIEntityTYpeConfiguration
を実装したクラスを作る Configure
で適切に生成する DbContext.cs
のOnModelCreating
でConfiguration
を呼び出す - モデルで
public ICollection<City> Cities { get; set; }
などをnew()
していると怒られる模様
SportsStore
の場合
Models/SeedData.cs
などを適切に作る Program.cs
で読み込む
| 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);
}
|
主キーが外部キーの場合のモデルの書き方
| public class Shop
{
public int Id { get; set; }
public string Name { get; set; } = default!;
}
|
| public class OrderNumber
{
[Key] public int ShopId { get; set; }
[ForeignKey("ShopId")] public Shop Shop { get; set; } = default!;
public int Number { get; set; }
}
|
データベースのリセット・削除
| dotnet ef database drop --force --context <データベースコンテキスト>
|
認証
ASP.NET Core Identity
を使う.
認証用ユーザーにリレーションを張る
- 参考記事
- 参考実装: 2023/01
- 次のようなモデルを作る.
ApplicationUser : IdentityUser
がAspNetUsers
テーブルになる.
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; }
}
|
認証用ユーザーの主キーを変える
マイグレーションの作成
- ローカルインストールした
EF Core
を使う場合は次の通り.
| dotnet dotnet-ef migrations add InitialCreate
dotnet dotnet-ef migrations add Rating
|
| dotnet dotnet-ef migrations add InitialCreate -c <データベースコンテキスト>
|
EF Core
のツールをグローバルインストールしている場合は下記コマンド.
| dotnet ef migrations add InitialCreate
|
マイグレーションの自動適用
- 2021 Entity Framework Core in Action, P.149
- 参考:
Program.cs
のConfigure
メソッド内でMigrations
を適用する.
| using (var context = new SchoolContext(
services.GetRequiredService<DbContextOptions<SchoolContext>>()))
{
context.Database.Migrate();
}
|
マイグレーションの反映
| dotnet dotnet-ef database update
|
マイグレーションのundo
| dotnet ef migrations remove
|
リバースエンジニアリング・データベースファースト・スキャフォールド
- データベース側の準備一例
A5SQL
を使って作ったER図doc/mydba5er
からSQLを生成してdb/init/init.sql
に置く - SQLで
ID
をId
に置換する - データベースを初期化: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
| 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
を使う
| var firstBook = await _context.Books()
.Include(b => b.Reviews)
.FirstOrDefaultAsync();
|
リレーションの読み込み: 明示的な読み込み
- 2021 Entity Framework Core in Action, P79
| var firstBook = _context.Books.First();
_context.Entry(firstBook)
.Collection(b => b.AuthorsLink).Load();
|
リレーションの読み込み: Select
- 2021 Entity Framework Core in Action, P80
| 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
という名前のデリゲートを宣言
| 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));
|
| 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));
|
JsonSerializer
でUTF-8
エンコードを外して日本語を正しく表示させる
| var json = JsonSerializer.Serialize(creditVerifyCardRequest);
_testOutputHelper.WriteLine("👺 リクエストのJSON(日本語文字化け)");
_testOutputHelper.WriteLine(json);
|
| var options = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var json2 = JsonSerializer.Serialize(creditVerifyCardRequest, options);
_testOutputHelper.WriteLine("👺 リクエストのJSON2(日本語文字化けしない)");
_testOutputHelper.WriteLine(json2);
|
GUID
| // System.Guid.NewGuid()
System.Console.WriteLine(Guid.NewGuid());
|
SHIFT_JIS
またはWINDOWS-31J
を扱う
.NET CoreはASCII
かUTF-8
しか使えない. 次のようなコードを書くとSHIFT_JIS
文字列が扱える. 少なくとも英数だけならWINDOWS-31J
もSHIFT_JIS
扱いでよいようだ.
| 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);
|
| var shiftJis = Encoding.GetEncoding("shift_jis");
var content = shiftJis.GetString(await response.Content.ReadAsByteArrayAsync());
var resultDictionary = _sut.ShiftJisByteArrayToDictionary(await response.Content.ReadAsByteArrayAsync());
|
UTC
を適切なタイムゾーンに変換する
| 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
環境変数を取得する
| var envRegion = Environment.GetEnvironmentVariable("REGION");
var envCognitoUserPoolId = Environment.GetEnvironmentVariable("COGNITO_USER_POOL_ID");
var envClientId = Environment.GetEnvironmentVariable("CLIENT_ID");
|
警告をエラー扱いにするcsproj
の設定
| <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の取得
| 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;
}
|
| public record FirstName(string Value) : ConceptAs<string>(Value);
public record LastName(string Value) : ConceptAs<string>(Value);
public record SocialSecurityName(string Value) : ConceptAs<string>(Value);
|
| 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);
}
|
配列の初期化
| int[] table = new int[100];
|
| // 100個の要素を-1で初期化
int[] table = Enumerable.Repeat<int>(-1, 100).ToArray();
|
| 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
DynamoDB
へのアクセス
- AWS SDK for .NET を使った DynamoDB へのアクセス方法
AmazonDynamoDBClient
DynamoDB
の開発ガイドのチュートリアルで紹介されている - 取得できる
Items
のクラスはList<Dictionary<string, AttributeValue>>
ORM
は自力で対応
DynamoDBContext
DynamoDB
開発ガイドではオブジェクト永続性モデル
と呼ばれる ScanAsync
などの処理時に型パラメータとしてマッピングするクラスを指定する
DynamoDBContext
使用時のテーブル名指定 - 基本的にはマッピング先のクラスの
DynamoDBTable
属性の引数でテーブル名を指定 AWSConfigsDynamoDB.Context.AddMapping
メソッドでもテーブル名が指定できる - テーブル名を環境変数や設定ファイル等から取得して設定するならこれが便利
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 | using Amazon;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.Util;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var client = new AmazonDynamoDBClient();
var context = new DynamoDBContext(client);
AWSConfigsDynamoDB.Context.AddMapping(new TypeMapping(typeof(SampleTableItem), "SampleTable"));
var items = await context.ScanAsync<SampleTableItem>(null).GetRemainingAsync();
}
}
class SampleTableItem
{
// PartitionKey = user_id (String)
// SortKey = item_id (String)
[DynamoDBHashKey("user_id")]
public string UserId { get; set; }
[DynamoDBRangeKey("item_id")]
public string ItemId { get; set; }
}
|
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
| let undefined<'T> : 'T = failwith "Not implemented yet"
let stub1 (x : int) : float = undefined
let stub2 (x : 'T) : 'T = undefined
|
| let undefined<'T> : 'T = raise (NotImplementedException())
|
fsx
実行: 競プロ用実行
| dotnet fsi hoge.fsx
dotnet fsi hoge.fsx < input.txt
|
fsx
で他のfsx
を読み込む
| #load "Script1.fsx"
open Script1
|
Fable
メモ
| dotnet new --install Fable.Template
dotnet new fable
|
Json
- 参考
C#
のコードを参考にすればよい mutable
と<-
を使うのが大事
| 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)
|
Rider
でF# interactive
を実行
- 上部メニューの
ツール
からF# インタラクティブ
を選ぶ - (
Mac
では)領域を選択してからCmd+Enter
でインタラクティブにコードを送信して実行できる
Stack overflow
AtCoderの木DPで, Pythonコードに沿って実装してみたらStack overflowしたのでその対処の記録.
- 再帰関数で
stack overflow
しないように末尾再帰に書き直すのはF#だけでなく関数プログラミングで必須技能なので練習しよう - 再帰の回数の上限ではなくスタックに割り当てられるメモリの量に制限がある
- 管理は
.NET
ではなくOS - Linuxなら
ulimit -s
で変更できるはず - 参考: Python版
String
にはreverse
がない(?)ので代替策を見つけた
どなたか有識者の方, いい感じのメソッドがあれば教えてほしい. 今回はProject Euler Problem 4の回文数の問題を解いているときに出くわした. どうもStringにはreverseメソッドがないらしい. 検索した結果, 今回はこのページのコピペとして次のコードを採用した.
| 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__
を指定する
| #r "nuget: FSharp.Data"
open FSharp.Data
type Comments = JsonProvider<"sampleResponse.json", ResolutionFolder=__SOURCE_DIRECTORY__>;;
|
関数に型をつける
ふつうの方法
| let f (i:int) (j:int): int = i * j
|
関数シグネチャを使う方法
| type MyFuncSignature = int -> int -> int
let myFunc: MyFuncSignature = fun i j -> i * j
|
ラムダを使う方法
| let f: int -> int -> int =
fun i j -> i * j
|
空シーケンスの判定, 特にmatch
- 空シーケンスのリテラルがないので
s when Seq.isEmpty s
のように書く.
| 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)
|
処理時間測定
| open System.Diagnostics
let sw = System.Diagnostics.Stopwatch()
sw.Start()
// 処理
sw.Stop()
stdout.WriteLine(sw.Elapsed)
|
遅延リストはとりあえずSeq
- 残念ながらHaskellのリストなどと同じ書き味にはならない
| Seq.initInfinite (fun i -> 2 * i + 1)
|
ファイルの読み書き
C# (.NET)
のメソッドを使おう - JSONの場合の注意
- 既定のエンコーダーと比較して
UnsafeRelaxedJsonEscaping
エンコーダーは文字をエスケープせずにそのまま渡すことについてより寛容 <
, >
, &
, '
などHTMLに影響する文字はエスケープされない s.Replace(@"\u002B", "+").Replace(@"\u003C", "<").Replace(@"\u0027", "'").Replace(@"\u0026", "&").Replace(@"\u003E", ">")
で置換する
| let fileName = "1.tmp.txt"
System.IO.File.WriteAllText(fileName, someString, System.Text.Encoding.Default)
|
複数行の文字列, ヒアドキュメント
C#と同じように書ける模様. 特に三重引用符または「@+クオート」で書ける. ついでに逐次的リテラル文字列の概念があり, 後者の「@+クォート」形式がそれ. 例を引用しておく.
| ".¥¥hoge¥¥hoge¥¥hoge.txt" // ふつうの文字列
@".¥hoge¥hoge¥hoge.txt" // 逐次的リテラル文字列
|
プロジェクト作成
VSCodeを前提にする.
- 下記の内容のファイルを
setup.txt
として作る. MySolution
とMyProject
は適宜修正. 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
|
PowerShell
Macへのインストール
| brew install powershell/tap/powershell
|