本家動画はこちら
EP01~05はこちら
EP06~09はこちら
EP10でREST API扱うんだけど、API叩くにもちょうどいいのがないので、この人の別動画
ASP.NET Core Web API + Entity Framework Core
の基本を番外でまとめていくよ。
番外①:REST APIの作り方
- ASP.NET CoreでREST APIを作るのは簡単だよ。
- まずは専用のプロジェクトを作るパターンから見てみよう。
- プロジェクトを新規作成して「ASP.NET Core Webアプリケーション」を選択するよ。プロジェクト名は何でもいいよ。
- 次に「API」を選択してプロジェクトを作成するよ。するとサンプル付きでプロジェクトができるよ。
- とりあえずこのまま何も変更せずに実行してみよう。すると「https://localhost:(ポート番号)/weatherforecast」に遷移してJSONが返ってきたね。
- それじゃコードについて説明していこう。
- まずは「Controllers」フォルダ内の「WeatherForecastController.cs」。これがデータを返してる本体だよ。
- クラス名が「
WeatherForecastController
」ってなってるけど、このクラス名のControllerより前の部分が[Route("[controller]")]
の[controller]
に該当するよ。試しにクラス名を「HogeController
」に変えて実行してみると、初期遷移の「https://localhost:(ポート番号)/weatherforecast」はエラーになって「https://localhost:(ポート番号)/hoge」にアクセスするとJSONが返ってくるのがわかるね。 - どのメソッドがJSONを返しているかというと
[HttpGet]
属性がついたIEnumerable<WeatherForecast> Get()
が返してるよ。このメソッドみたいに[HttpGet]属性つけてIEnumerable<(任意の型)>ってやれば勝手にクラスをJSONに変換して返してくれるんだね。 - ちなみにこのAPIはBlazorとかASP.NET MVCとかにも追加できるよ。
- ただその場合、「Startup.cs」の編集が必要で、
ConfigureServices
メソッドにservices.AddControllers();
の追加と、Configure
メソッドにapp.UseHttpsRedirection();
、app.UseAuthorization();
、endpoints.MapControllers();
の追加が必要だよ。細かい追加位置はサンプルを確認してね。
(あとはこの人が公開してるサンプル(CuriousDrive/BookStores - GitHubの「SalesController.cs」)見たほうが早いかも )
(↑のサンプルよく見ずにVisualStudioが作るテンプレベースにしてたらハマった・・・)
(VisualStudioが作るテンプレだとAPIの戻り値がIEnumerable<WeatherForecast>
になってるけど、これだとGetFromJsonAsync
で戻り値が取れん。APIの定義側も非同期に対応するためにTask<ActionResult<IEnumerable<WeatherForecast>>
ってせんとあかんかった)
(EntityFrameworkCoreと組み合わせたらもっと簡単な方法があった…(´・ω・`))
(DBコンテキスト継承したクラス作ってEntityFrameworkCoreで読み書きできる状態にした上で、プロジェクト内の任意のフォルダ右クリック→追加→コントーローラー→共通→API→Entity Frameworkを使用したアクションがあるAPIコントローラー、で一発で参照/追加/削除のWeb APIが生成された…)
(↑で生成したコントローラー、なんかうまく動かん…)
(「Startup.cs」の public void ConfigureServices(IServiceCollection services)
の中に services.AddScoped<AuthorContext>();
を追加する必要があったわ。)
(クラスにバリデーションつけとけば勝手に文字列長とかチェックしてエラー返してくれるのイイね)
(ただやっぱり勉強用コードとは言え、DBエラーとか例外はちゃんとcatchしてエラーメッセージ返さないとエラー発生時が大変だわ)
番外②:EntityFrameworkCore
(動画は長いしSQLServer立てる必要もあるからSQLite使って今までの書き方みたく書いていこう。参考にしたのは
Entity FrameworkでSQLiteを利用する方法とは とか
Database.EnsureCreatedおよびDatabase.Migrateを呼び出す方法と場所 とか
Creating a Model for an Existing Database in Entity Framework Core とか
VSCodeでRESTクライアントを使って効率的にAPIを呼び出す。 とか
C# EntityFrameworkでIDENTITY属性を無効化する とか。)
- EntityFrameworkを使って、今まで使ってた
Author
クラスのデータをデータベースに保存してみよう。 - RDBMSはインストールとかいらないSQliteを使うよ。
- SQliteはRDBMSの一種でライブラリだけで動いてファイル単位でDBを持てるやつだよ。
- 今までのプロジェクトに追加してもいいけど、わかりやすいようにコンソールアプリのプロジェクトを別で作ろう。
- プロジェクトを作ったら
Author
クラスのソースをこのプロジェクトに追加しよう。 -
Author
クラスのAuthorId
には[Key]
属性を付与しよう。こうすることでプライマリキーになって自動採番(SQLiteならAUTOINCREMENT)されるようになるよ。 - ちなみに自動採番したくない場合は追加で
[DatabaseGenerated(DatabaseGeneratedOption.None)]
を属性追加すればいいよ。 - 次にNugetで
Microsoft.EntityFrameworkCore.Sqlite
を追加するよ。 - 続いてコードを修正するよ。
using System.ComponentModel.DataAnnotations.Schema;
とAuthor
の前に[Table("Authors")]
を追加するだけだよ。
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;//←追加
namespace EntityFrameworkTest {
[Table("Authors")]//←追加
public class Author {
- 続いて
DbContext
を継承したクラスを作るよ。
public class AuthorContext : DbContext {
public DbSet<Author> Authors { get; internal set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder){
var connectionString = new SqliteConnectionStringBuilder { DataSource = @"C:\tmp\sample.db" }.ToString();
optionBuilder.UseSqlite(new SqliteConnection(connectionString));
}
}
- これで準備はOK。DBを読み書きしてみよう。
static void Main(string[] args) {
// コンテキスト・クラスを作成
using (var db = new AuthorContext()) {
// スキーマとかテーブルが無かったら作るよ。
db.Database.EnsureCreated();
//サンプルに山田さんを100人
for (var j = 0; j < 100; j++) {
//AuthorIdは自動採番されるので未指定でいいよ
db.Authors.Add(new Author
{
FirstName = "太郎",
LastName = "山田",
City = "東京",
EmailAddress = "taro@tokyo.com",
Salary = 10000000,
Phone = "03-1234-4567"
};);
}
// DBへ保存
int recordsAffected = db.SaveChanges();
// 順に取り出して表示
foreach (var i in db.Authors) {
Console.WriteLine("AuthorId: {0} , FirstName: {1} , LastName: {2}", i.AuthorId, i.FirstName, i.LastName);
}
// キー入力待ち
Console.ReadKey();
}
}
- ホントにDB読み書きしてるのか不安になるレベルのコードの少なさだけど、一回実行した後
EnsureCreated
からSaveChanges
までをコメントアウトしてもちゃんとデータが表示できてるから、ちゃんとDB読み書きしてることがわかるね。 - コードからDBを作ったけど、今度はDBからコードを作ってみよう。
- まずは別プロジェクトのコンソールアプリで 「EntryFrameworkTestFromDB」 っていうのを作ってみよう。
- NuGetで
Microsoft.EntityFrameworkCore.Sqlite
とMicrosoft.EntityFrameworkCore.Tools
を追加するよ。 -
パッケージマネージャコンソール(表示されてなければメニューの「表示」→「その他のウィンドウ」→「パッケージマネージャコンソール」から表示できるよ)にコマンド
Scaffold-DbContext "DataSource=C:\tmp\sample.db;" Microsoft.EntityFrameworkCore.Sqlite
を実行してみよう。 - 「Author.cs」「sampleContext.cs」が生成されたね。
- 「sampleContext.cs」だと名前がいまいちなので、名前は変えておこう。
- 「sampleContext.cs」を開いてクラス名
sampleContext
を右クリック、「名前の変更」を選択しよう。 -
sampleContext
がハイライトされるのでクラス名をAuthorContext
に変更しよう。 - 「シンボルのファイル名を変更する」にチェックが入っていることを確認して「適用」をクリックするとファイル名も含めてプロジェクト内の
sampleContext
がAuthorContext
に置き換わるよ。 - 同ファイルにはプリプロセッサ
# warning
で「ソースコードの中に接続文字列を入れないようにしようね」みたいな警告が出るけど、今回は無視するので削除しておこう。 - あとはメイン関数に「EntryFrameworkTest」で書いた表示プログラムを張り付けてみたらちゃんとデータが表示されることがわかるね。
EP10:REST APIの呼び出し
- 今回はREST APIの呼び出し方について説明するよ。
- 手順としては
@inject HttpClient
を追加して~~GetJsonAsync
GetFromJsonAsync<T>
、PostJsonAsync
~~~~PostAsJsonAsync
PostAsync
、DeleteAsync
、PutJsonAsync
~~~~PutAsJsonAsync
~~PutJsonAsync
でREST APIを叩くだけだよ。
(動画ではGetJsonAsync
とPostJsonAsync
とPutJsonAsync
やったけど、.NET5でやってたらGetFromJsonAsync
とPostAsJsonAsync
とPutAsJsonAsync
やった。名前変わったんかな?)
(あと日本語とか含んでると文字のエンコード処理がうまくいかないからPostJsonAsync
とPutJsonAsync
は使えんかった。)
- 別の動画で作った本屋さんWebAPIを使って実際にやってみよう。
- まずは「Startup.cs」の
ConfigureServices
にservices.AddSingleton<HttpClient>();
を追加するよ。 - 続いて使いたいページに
@inject HttpClient Http;
を追加するよ。 - あとは
GetFromJsonAsync<T>
で指定したURLを叩くだけだよ。
(「_Imports.razor」に@using System.Net.Http.Json
追加せな「GetFromJsonAsyncとか知らんで」って言われた。)
(無料の天気予報REST APIで試そうかとも思ったけど、これ用で作った方がシンプルかなぁ)
(→番外作成。地味にハマった・・・)
- 今回の場合Authorのリストを取得したいから
GetFromJsonAsync<LIST<Author>>
となるね。 - JSONのキー名をそのままクラスのプロパティ名にしたクラスを作っておけば後は勝手に変換してくれるよ。
- あとこの実行は非同期だからnull参照して落ちないように戻り値用変数は
new List<Author>
で初期化しておく必要があるね。
(自分で試したときは一々別プロジェクト立ち上げるのが面倒だったので、同じプロジェクトにコントローラー追加した。なので呼び出しは↓みたいな感じに。)
List<Product> ret = new List<Product>;
ret = await Http.GetFromJsonAsync<List<Product>>(NavigationManager.BaseUri + "Values");
- あと取ったデータは
authirKust.OrderByDescending(auth => auth.AuthorId).ToList<Author>();
で降順に並べておこう。 - それじゃ実行してみよう。ちゃんとAPIで取ってきたデータを表示しているのがわかるね。
- 続いて値の設定に移ろう。
-
SaveAuthor
関数のところでPOSTするようにするよ。(動画ではPostAsJsonAsync
使ってるけど、日本語含んでるとエンコードがうまくいかないのかエラーになるのでPostAsync
使わないとダメそう?あと、動画では書いてないけど、POSTの結果でJSONが返ってくる場合はReadFromJsonAsync
で結果を読んでデシリアライズが一発できるみたい)
var json = JsonSerializer.Serialize(x, new JsonSerializerOptions {
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
});
var content = new StringContent(json, Encoding.UTF8, "application/json");
var ret = await Http.PostAsync(NavigationManager.BaseUri + "api/Authors", content);
var response = await ret.Content.ReadFromJsonAsync<Author>();
-
実行してみると正常にデータは更新されているはずなのに表示が更新されないのがわかるね。
-
これはデータを取り直して
StateHasChanged()
で画面に対して更新通知を行っていないからだよ。 -
POSTした後にデータを取り直して
StateHasChanged()
を呼んでみよう。 -
実行してみるとちゃんとデータが更新されることがわかるね。
-
次は削除と更新をしてみよう。
(動画じゃ分けてるけどPUTとDELETEの違い程度なので一緒で。) -
まずは表の各行に「更新」と「削除」ボタンを付けるよ。
(動画じゃHTMLタグにスタイル直書きしてるけどイマイチだなぁ…~~せっかくだしSCSS試してみるか。~~それより先にBootstrap覚えよ)
とほほのBootstrap 4入門
(杜甫々さんのページ、HP作るのにHTML覚えた(1998年頃)以来に見るけどすごいコンテンツ増えてるなぁ)
(Bootstrap使うのはテーブルとボタンかなぁ。動画だとハイパーリンクだけどボタンにしとこ)
<table class="table table-striped">
<thead class="thead-lignt">
<tr>
<th>ID</th>
<th>First name</th>
<th>Last name</th>
<th>City</th>
<th>Email</th>
<th>更新/削除</th>
</tr>
</thead>
<tbody>
@foreach (var a in author_list)
{
<tr>
<td>
@a.AuthorId
</td>
<td>
@a.FirstName
</td>
<td>
@a.LastName
</td>
<td>
@a.City
</td>
<td>
@a.EmailAddress
</td>
<td>
<button type="button" class="btn btn-outline-primary btn-sm" role="button" @onclick="(() => EditAuthor(a.AuthorId, author))">Edit</button> |
<button type="button" class="btn btn-outline-danger btn-sm" role="button" @onclick="(() => DeleteAuthor(a.AuthorId))">Delete</button>
</td>
</tr>
}
</tbody>
</table>
- 続いて更新と削除のコードを書くよ。表示更新はどっちも同じなので関数化したよ。
private async Task UpdateTable() {
author_list = await Http.GetFromJsonAsync<List<Author>>(NavigationManager.BaseUri + "api/Authors");
StateHasChanged();
}
public async Task EditAuthor(int _id, Author _author) {
_author.AuthorId = _id;
var json = JsonSerializer.Serialize(author, new JsonSerializerOptions {
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
});
var content = new StringContent(json, Encoding.UTF8, "application/json");
var ret = await Http.PutAsync(NavigationManager.BaseUri + "api/Authors/" + _id.ToString(), content);
if (ret.IsSuccessStatusCode) {
//今は結果を返さないのでひとまずコメントアウト
//var response = await ret.Content.ReadFromJsonAsync<Author>();
}
await UpdateTable();
}
public async Task DeleteAuthor(int _id)
{
var ret = await Http.DeleteAsync(NavigationManager.BaseUri + "api/Authors/" + _id.ToString());
await UpdateTable();
}
(動画じゃSaveAuthor
にif文追加してPOSTとPut分岐してるな。しかも動作は「Edit」押したらフォームに値設定→Saveで更新 か、上記だとフォームに入力→Edit押したら更新になるな。けどまぁRESTの動き見るだけだからまぁいっか)
EP11:認証(すぐ使えるやつ)
- 今回は簡単にすぐ使える認証について説明するよ。
- Blazorのプロジェクトを作るときに認証オプションが表示されるやつの話だよ。
- 認証してユーザー情報をDBに保存するよ。
- あと今回説明する認証方法の長所/短所の話もするよ。
- 他に作成時に設定しなかった場合に後から追加する話もするよ。
- それじゃ実際に作っていってみよう。
- Blazorのプロジェクトを作るよ。プロジェクト名はOutOfBox(すぐ使える)にしようか。
- 「新しいBlazorアプリを作成します」の画面まで行くと右に「認証」っていう表示があってデフォルトは「認証なし」になってるね。
- ここで変更をクリックすると認証を選択できるよ。
- 「個別のユーザーアカウント」は自前でアプリの中にユーザーのDBを持つか、Azure AD B2Cを使う方式だよ。
- 「職場または学校のアカウント」はオンプレのADかAzure ADを使う方式だよ。
(業務アプリ作るならAzure AD使う方式が重宝しそう。SAMLとかOpenID Connectにも対応してくれたらうれしいんだけどなぁ)
- 「Windows認証」はIISの認証方式に従うやつだよ。
- 今回は「個別のユーザーアカウント」を選択して自前でアプリの中にユーザーのDBを作ってみよう。
- まずは何も考えず動作を見てみよう。実行すると右上に「Log in(ログイン)」があるのでクリックしてみよう。
- 自動で生成されたRazorページが表示されるよ。
- ユーザーは誰も登録してないので「Register as a new user(新しいユーザーを登録する)」をクリックしよう。
- 実際に登録を実行してみると「DBを開くことができません」っていう例外が発生するね。
(パスワード要件(6文字以上、大文字小文字記号必須)がデフォルトで設定されてるのいいね)
- DBを構成する必要があるよ。
- ちなみにこの「個別のユーザーアカウント」認証ではEntryFrameworkを使ってSQL Serverにユーザー情報を格納しているよ。
(動画ではSQLServerって言ってるけど面倒だしSQLiteかなぁ)
(SQL Serverって言ってもVisual Studioと一緒にインストールしたような気がするLocalDBだから別途インストールする必要ない(※)のか)
※Visual Studioのインストールの時にオプション選んだかもしれないけど覚えてない
参考:
MicrosoftSQL Server 2016 Express LocalDB で気軽に DB を試す
(Visual Studio 2017からSQL Server Express 2016 LocalDBを使ってみる)[https://qiita.com/akabei/items/7f62056e44dd2d44f703]
- この画面の「Apply Migrations」をクリックすると「00000000000000_CreateIdentitySchema.cs」のコードが実行されてスキーマとかテーブルとかDBが全部作られるよ。
(ブラウザ翻訳便利。そういえば1から作るのにMigrations(=移住/移行)ってなんか違和感あるよね。コードからDBへの移行ってこと?)
- それじゃ「Apply Migrations」をクリックして「Try refreshing the page」と表示されたらページを更新してみよう。ログインができた状態で初画面に移るね。いったんログアウトしてログインできるか見てみよう
(うちの環境じゃ「Register confirmation(登録確認)」画面で「メール送れんかったよ、設定してね」みたいな表示になった。微妙なバージョンの違いかな?登録はできてるみたいでログインはできる)
- さっき登録したメールとパスワードを入力して「Log in」をクリックするとログインできて右上にメアドが表示されてるのがわかるね。
(何回かやってたらログイン失敗するようになった。ググってみたら「Startup.cs」のConfigureServices
内の以下のoptions.SignIn.RequireConfirmedAccount
をfalse
に設定してメールによる確認を無効にする必要があるとか)
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
↓
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>();
- このログイン情報がどこに格納されてるか見てみよう。
- 「appsettings.json」に接続文字列が書かれていてSQL Server Express LocalDBにつながってるのがわかるね。
- サーバーエクスプローラーでつなげてスキーマを見てみよう。
(SQL Serverオブジェクトエクスプローラーじゃなくて汎用性を考えてサーバーエクスプローラーを使うんやね)
- サーバーエクスプローラーで「データベースへの接続」をクリックして出てきたダイアログのサーバー名に「(localdb)\mssqllocaldb」と入力するよ。コードの方からコピペするときはエスケープシーケンス表記になってるので気を付けてね。
- DBはなんかGUIDがくっついたDBができてるのでそれを選ぶよ。
(このGUIDって↓と関係してるっぽいな。ソリューション内Grepかけたらcsprojのファイルの中の「UserSecretsId」ってやつと一致した)
ASP.NET Core での開発におけるアプリシークレットの安全な保存
-
AspnetUser
っていうテーブルにユーザーが格納されてるよ。テーブルを右クリック→「テーブルデータの表示」から中身を見てみよう。 -
UserName
とEmail
にメールアドレスが、パスワードに暗号化されたパスワードが入ってるのがわかるね。こんな感じで認証に必要な機能は一通りそろってるよ。
(暗号化って言ってるけどハッシュやんな?復号化出来たら危ないんやけど…)
- Role(役割)も設定できるから「ユーザーAは画面Xと画面Yが表示可能」「ユーザーBは画面Xのみ表示可能」とかもできるよ。
- プロファイルページもあってそこで電話番号入れたりメアド変えたりパスワード変えたり2要素認証追加したりアカウント削除したりできるよ。
- 画面に戻ってみようログインしてない状態だと「Register(登録)」「Login」の二つがあるね。このエイリアスがどのコードと対応してるか探してみよう。
- Areasフォルダの下にIdentityフォルダがあって、さらにその下にPagesフォルダがあるんだけど、ここにあるのは二つしかないね。
- 登録画面とかプロファイル管理画面が何故ないかというと、これらの画面は
Microsoft.AspNetCor.Identity.UI
に含まれてるからで、プロジェクトごとに追加したりする必要はないからだよ。独自のページを作ることもできるよ。
(詳しくは書いてないけど、「Identity」フォルダ右クリック→追加→新規スキャフォールディングアイテム→ID、で登録できるみたい)
- 次は実際にどんな風にユーザー認証してるのかを見てみよう。まずは「Startup.cs」から。
-
Configure
メソッドの中で、app.UseRouting()
の後にapp.UseAuthentication()
とapp.UseAuthorization()
を呼んでるね。 - 今回はプロジェクト作成時に認証ありで作成したから最初からあるけど、後から認証を追加する場合は自分で追加しないといけないよ。
- 認証の処理とかを実際に行ってるオブジェクトがどれかというと
ConfigureServices
の中でAddScoped
で追加されてるAuthenticationStateProvider
から派生したRevalidatingIdentityAuthenticationStateProvider<IdentityUser>
だよ。 - このオブジェクトで認証されてるかどうかとか状態を保持してるよ。
- 実装である
RevalidatingIdentityAuthenticationStateProvider
の定義を見てみよう。 - メソッド
ValidateAuthenticationStateAsync
内で認証状態を検証してるよ。この中で呼ばれてるValidateSecurityStampAsync
の中でGetUserAsync
を呼んでユーザー情報を取得してるよ。
(この辺の説明、自動翻訳だとメソッド名まで翻訳するから訳見ても意味が通らん…。この辺りは飛ばして、細かい仕組みの面は↓見てまた今度勉強しよ)
(CascadingAuthenticationState(数珠繋ぎの認証状態)って何やろ?)
(ASP.NET Core Blazor の認証と承認)[https://docs.microsoft.com/ja-jp/aspnet/core/blazor/security/?view=aspnetcore-5.0]
- 次に、画面の動きを見てみよう。ログインしてない状態ではRegisterとLoginが、ログインするとメールアドレスとLogoutが表示されるね。
- これがどのように実装されているか「MainLayout.razor」を見てみよう。「About」の上に
LoginDisplay
っていうRazorコンポーネントを呼んでるのが見て取れるね。 - それじゃ「LoginDisplay.razor」を見てみよう。ここには3つの重要なコンポーネントが書かれてるよ。
-
AuthorizeView
とAuthorized
とNotAuthorized
だね。これは直感的にもわかると思うけど認証済みならAuthorizedの中身を表示、認証していないならNotAuthorizedの中身を表示っていう動きになるよ。 - ここには書かれてないけど
Authorizing
って言うのもあるよ。認証に時間がかかる場合に使うといいよ。 - それじゃ今回説明した認証の長所と短所をまとめてみよう。
- 長所は「セットアップが簡単(プロジェクト作成時に選択するだけ)」「小さいプロジェクトに向いてる」「DBの設計がいらない」「登録とかログインがすぐ使える」だね。
- 短所は「EntryFrameworkが必須」「APIを利用した設計ができない(データアクセスをAPIにしようとしたとき、APIの認証として使えない)」「古いrazorページを使う(cshtmlのこと?)」「既に存在するユーザーDBからの移行が難しい」だね。
- 次のエピソードでこれらの短所をカバーする方法を紹介するよ。
EP12:Authentication | CustomAuthenticationStateProvider
- このエピソードではCustomAuthenticationStateProviderについて説明するよ。
EP13:Layouts | Login Pages
EP14:HttpClient | Login User
EP15:IHttpClientFactory | Login User
EP16:Sending JWT token & Building Request Middleware
EP17:Register User & Generate JWT
EP18:Role-based Authorization
EP19:Policy-based Authorization
EP20:Procedural Logic | Authentication & Authorization in C#
EP21:Templated Components | Html Table
EP22:Razor Components | EventCallback
EP23:Event Handling
EP24:GridView Header Filter
EP25:Gridview Paging
EP26:Spinner or Activity Indicator : EP26
EP27:Code Faster Using dotnet watch run
EP28:Deploy to IIS
EP29:Deploy to Azure App Services
EP30:Handling Exceptions
2022/8/22更新
業務忙しさにほったらかしにしてたら1年経ってしまった… また頑張ろ。
(ひとまず見出しの新記法対応のみ実施)