コンテンツにスキップ

.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

dotnet-user-secrets

dotnet NuGetキャッシュの削除

1
dotnet nuget locals --clear all

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

1
dotnet --list-sdks

dotnet コマンドサンプル

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

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

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

dotnet ソリューション初期化

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

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

1
dotnet sln add <HogeProject>

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

1
dotnet new --list

dotnet バージョンを指定する

1
2
dotnet new globaljson
dotnet --list-sdks

dotnet バージョンを調べる

1
dotnet --version

dotnet パッケージを追加する

1
dotnet add package <hoge> --version

dotnet パッケージを削除する

1
dotnet remove package <hoge>

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

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>

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.5.255402/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.5.255402/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

読書メモ, 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

初期化

1
2
dotnet tool restore
dotnet restore

導入メモ: 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"
  },

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

Blazor WebAssembly: ソリューション生成

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

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: 容量削減

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

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

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: 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

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 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に設定する.

タグヘルパー

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

タグヘルパー 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();

メール送信 MailKit

リクエストへの容量制限

  • [RequestSizeLimit(1048576)]

リリース時の容量削減

  • <PublishTrimmed>true</PublishTrimmed>

ルート制約の非同期処理

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

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

例外処理

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;
}

大文字小文字の無視: 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; }
}

データベースのリセット

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

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));

GUID

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

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

タイムゾーンIDの取得

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

AWS

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