ASP.NET

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

1章

1
dotnet --list-sdk

2章

1
dotnet tool install -g LiveReloadServer
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章

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

開発中に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

 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"

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

1
dotnet tool install dotnet-ef --version 6.0.14
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
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<MyDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MyDbContext") ??
                      throw new InvalidOperationException("Connection string 'MyDbContext' 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=app.db;Cache=Shared"
  },

PostgreSQL

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

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

1
2
dotnet new tool-manifest
dotnet tool install Microsoft.Web.LibraryManager.Cli
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

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

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

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の共存

 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のマイグレーション

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

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

Program.cs WebApplicationBuilderのプロパティ

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

環境タグヘルパー

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

Blazor: 容量削減

1
2
3
4
5
6
7
  <PropertyGroup>
    <!-- 略 -->

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

ExceptionHandler

Identity: LockoutOptions

Identity: AspNetUserRolesへのシード登録

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: スキャフォールド

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

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: 認証追加

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

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

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

QRコードの生成

 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();
    }
}
1
2
3
4
<div class="text-center">
    <h2 class="display-4">Welcome</h2>
    <img src="@Model.ImageSrc" alt=""/>
</div>

Razor Class Library (RCL)

1
2
builder.Services.AddRazorPages();
app.MapRazorPages();

Razor Pages @page

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

Razor Pages @page ルート制約

Razor Pages addTagHelperディレクティブ

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

Razor Pages AutoMapper

Razor Pages 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

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を探す場所

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

Razor Pages ViewBag

Razor Pages ViewData

1
2
3
ViewData["Title"] = "Welcome!";

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

Razor Pages View Model

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

 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 名前つきハンドラーメソッド

 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 アクションの結果

Razor Pages カスタム検証属性

Razor Pages コメントの書き方

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

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

Razor Pages フィルター, IAsyncPageFilter

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

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

... 以下省略 ...
1
2
3
4
5
6
7
8
9
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning"
    }
  }
}

Razor Pages 部分ビュー

1
<partial name="_ProductViewDataPartial" for="Product" view-data="sampleViewData">

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

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

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

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

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

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

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

認可 [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での一元管理

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

認可 リソースへの認可

メール送信 MailKit

リクエストへの容量制限

リリース時の容量削減

ルート制約の非同期処理

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

例外処理