ASP.NET
読書メモ, 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
|
初期化
| dotnet tool restore
dotnet restore
|
導入メモ: 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 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<MyDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("MyDbContext") ??
throw new InvalidOperationException("Connection string 'MyDbContext' 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=app.db;Cache=Shared"
},
|
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>();
}
|
環境タグヘルパー
Blazor: フロントに送る容量をおさえつつBlazorを使う
- 参考
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: 容量削減
| <PropertyGroup>
<!-- 略 -->
<!-- Reduce output size -->
<InvariantGlobalization>true</InvariantGlobalization>
<WasmEmitSymbolMap>false</WasmEmitSymbolMap>
</PropertyGroup>
|
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: 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: パスワードハッシュの値を固定する
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: ユーザーごとのRole
取得
- 参考
UserManeger.GetRoles(TUser)
またはUserManeger.GetRolesAsync(TUser)
を使えばよい.
Identity: ユーザー名への一意制約
IdentityUser
(または継承)クラスに対して[Index(nameof(UserName), IsUnique = true)]
のようなアノテーションをつける
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 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 アクションの結果
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 ビューコンポーネント
- ファイル・データベース・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 リテラル文字列の表示
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 ロガーをテストでモックする
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);
|
URL末尾に常にスラッシュをつける
Web API IActionFilter
- 参考記事, 公式: ASP.NET Core Web API のエラーを処理する
HttpGet
などのHTTP
メソッド属性でエラー ハンドラー アクション メソッドをマークしてはいけない. - 明示的な動詞を使うと要求がアクション メソッドに届かない可能性がある.
Swagger/OpenAPI
を使うWeb API
の場合, エラー ハンドラー アクションを[ApiExplorerSettings]
属性でマークし, そのIgnoreApi
プロパティをtrue
に設定する.
タグヘルパー
- サーバー側のプロパティとブラウザーにレンダリングされるフォームコントロールの間に双方向のバインディングが作れる
タグヘルパー 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">
の指定が重要
タグヘルパー ファイルアップロード時の拡張子制限
認可 [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");
});
|
認可 リソースへの認可
メール送信 MailKit
リクエストへの容量制限
[RequestSizeLimit(1048576)]
リリース時の容量削減
<PublishTrimmed>true</PublishTrimmed>
ルート制約の非同期処理
ロガーなしでログを出力する
- いろいろあって
static
なクラスからDI
のロガーなしでログを出力したい. - 具体的には
AWS
上での調査用にこの状況が出てきた.
Console.WriteLine()
を使えばよい.
例外処理