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()を使えばよい.
例外処理