ASP.NET Core MVC - アクション名のAsyncサフィックスが削除されることを確認する

ASP.NET Core の破壊的変更 - .NET Core | Microsoft Docs

上記ドキュメントにある「コントローラアクション名からAsync サフィックスが削除される」ことを確認してみました。

MVC: コントローラー アクション名から Async サフィックスを削除 aspnet/AspNetCore#4849 への対処の一環として、ASP.NET Core MVC では、アクション名から Async サフィックスが既定で削除されます。 ASP.NET Core 3.0 以降、この変更はルーティングとリンク生成の両方に影響します。

ルーティング

次のコントローラのSampleAsyncアクションメソッドは~/default/sampleのURLで呼び出せますが、~/default/sampleasyncでは呼び出せません。

public class DefaultController : Controller {
    public async Task<IActionResult> SampleAsync() {
        await Task.Delay(0);
        return View();
    }
}
URLの生成

タグヘルパーやHTMLヘルパー、URLヘルパーでURLを生成するときは、アクション名からAsyncを取り除く必要があります。

うっかりAsyncを付けてしまいそう。

<a asp-action="Sample" asp-controller="Default">@Url.Action("Sample", "Default")</a>

<!-- 生成されるHTML -->
<a href="/default/sample">/default/sample</a>
ビュー名

ビュー名を省略した場合のViewResultは、Asyncを取り除いたアクション名のファイルを検索します。例えばSampleAsyncアクションではSample.cshtmlを検索します。

もちろんPartialViewResultも同じ動きでした。

public class DefaultController : Controller {
    public async Task<IActionResult> SampleAsync() {
        await Task.Delay(0);
        return View();
    }
    // 検索するcshtmlのパス
    // /Views/Default/Sample.cshtml
    // /Views/Shared/Sample.cshtml

    public async Task<IActionResult> SamplePartialAsync() {
        await Task.Delay(0);
        return PartialView();
    }
    // 検索するcshtmlのパス
    // /Views/Default/SamplePartial.cshtml
    // /Views/Shared/SamplePartial.cshtml
}
おまけ

AsyncアクションメソッドとAsyncを省略した同名のアクションメソッドを同じコントローラに定義できますが、実行してURLにアクセスするとAmbiguousMatchExceptionがスローされます。

注意しましょう。作らないかな。

public class DefaultController : Controller {
    // AmbiguousMatchExceptionがスローされる
    public IActionResult Sample() {
        return View();
    }

    public async Task<IActionResult> SampleAsync() {
        await Task.Delay(0);
        return View();
    }
}

SQL Server - シーケンスを使う

SQL Serverでシーケンスを使うサンプルです。

シーケンスを久しぶりに使おうと思ったらあまり覚えておらず、以前に試したので記事が残ってるかと探してみましたが、見つからなかったのでさらっと残しておきます。

ざっくり言うとシーケンスは連番を生成するオブジェクトです。 テーブルのIDENTITY列がオブジェクトとして独立したものというと伝わるでしょうか。

使い方のポイントは2つ。

  • シーケンスオブジェクトを用意する
    • create sequence
  • シーケンスオブジェクトから連番を取得する
    • next value for関数
    • (複数の値をまとめて取得するなら)sp_sequence_get_rangeストアドプロシージャ

確認していきましょう。

シーケンスオブジェクトの作成する

create sequence文でシーケンスオブジェクトを生成します。

-- シーケンスを作成
-- as {型}
-- start with {値}: next valueを呼び出す取得できる最初の値
create sequence dbo.SQ_ItemId
    as bigint
    start with 1;

start withは連番の開始値(最初に取得できる値)を指定します。この例では1から取得できます。

シーケンスオブジェクトから連番を取得する

next value for関数(これ関数でいいのか?)を使ってシーケンスオブジェクトから連番を取得してみましょう。 以下はselect文でのサンプルですがinsert文でももちろん利用できます。

-- 次の値を取得
select next value for dbo.SQ_ItemId;

連番を範囲で取得するにはsp_sequence_get_rangeストアドプロシージャを利用します。

-- 次の値を範囲で取得
declare @name nvarchar(776) = N'dbo.SQ_ItemId';
declare @size bigint = 5;  -- 取得する数
declare @first sql_variant;  -- 最初の値
declare @increment sql_variant; -- 増分

execute sp_sequence_get_range
    @sequence_name = @name,
    @range_size = @size,
    @range_first_Value = @first output,
    @sequence_increment = @increment output;

-- @firstと@incrementがあれば取得した連番がわかるはず
select @first, @increment;

シーケンスオブジェクトは、insertする前にIDの値を把握できる点が便利だと思います。 その必要がなければIDENTITY列のほうがお手軽でしょう。 他にもメリット・デメリットがあるのでシーンによって使い分けするものかなと思います。

参考

.NET Core - Type.IsAssignableFromのメモ

Type.IsAssignableFromは「メソッドを呼び出すインスタンス型の変数」に「メソッドの引数で指定した型のインスタンス」を割り当てることができるかどうかを判断します。

このメソッドはあるインターフェイスを実装しているか?あるクラスを派生しているか?をリフレクションで調べるときに使うと思いますが、文章で説明されてもすんなり頭に入ってこないですね。

たまに使うとどっちがどっちか忘れてるというか考えることが多いのでサンプルを残しておきます。

class Base {
}

interface IInterface {
}

class Derived : Base, IInterface {
}

var baseType = typeof(Base);
var derivedType = typeof(Derived);
var interfaceType = typeof(IInterface);

// Base型の変数にDerived型のインスタンスを割り当てることができる
Assert.True(baseType.IsAssignableFrom(derivedType));

// Derived型の変数にBase型のインスタンスを割り当てることができない
Assert.False(derivedType.IsAssignableFrom(baseType));

// Base型の変数にBase型のインスタンスを割り当てることができる
Assert.True(baseType.IsAssignableFrom(baseType));

// IInterface型の変数にDerived型のインスタンスを割り当てることができる
Assert.True(interfaceType.IsAssignableFrom(derivedType));

ちなみにあるクラスを派生してるかの判断だけでよければ(インターフェイスの実装は判断しなくてよければ)Type.IsSubclassOfでもよさげです。

// DerivedはBaseのサブクラスである
Assert.True(derivedType.IsSubclassOf(baseType));

// BaseはDerivedのサブクラスではない
Assert.False(baseType.IsSubclassOf(derivedType));

// Derived(インターフェイスの実装)はIInterfaceのサブクラスではない
Assert.False(derivedType.IsSubclassOf(interfaceType));

参考

ASP.NET Core - ファイルを扱う

ASP.NET Coreでファイルを扱うサンプルです。

ドキュメントはこのあたり。 docs.microsoft.com

任意の物理ファイルを扱うには、IFileProviderを実装したPhysicalFileProviderを使います。

// ルートフォルダ(=スコープ)を指定してファイルプロバイダを生成
var fileProvider = new PhysicalFileProvider(@"c:\temp");

// IFileInfoを取得して
var foundFile = fileProvider.GetFileInfo("test.txt");

// ファイルの存在チェックができたり
Debug.WriteLine(foundFile.Exists);
// True

// ファイルの内容を読み込んだり
if (foundFile.Exists) {
    using var stream = foundFile.CreateReadStream();
    using var reader = new StreamReader(stream);
    var text = await reader.ReadToEndAsync();
}

PhysicalFileProviderはインスタンス生成時に指定したフォルダ(スコープと呼ぶみたい)以外にアクセスできないようになっています。セキュアですね。

var fileProvider = new PhysicalFileProvider(@"c:\temp");

// スコープ外のファイルには存在していてもアクセスできない
var notFoundFile = fileProvider.GetFileInfo("c:\test.txt");
Debug.WriteLine(notFoundFile.Exists);
// False

// 戻り値はNotFoundFileInfoというクラス
Debug.WriteLine(notFoundFile.GetType());
// Microsoft.Extensions.FileProviders.NotFoundFileInfo

プロジェクト内のファイルにアクセスするファイルプロバイダは用意されています。

  • プロジェクト直下のwwwrootフォルダ - IWebHostEnvironmentのWebRootFileProviderプロパティ
  • プロジェクト直下のフォルダ(Startup.csなどがあるフォルダ) - IWebHostEnvironmentのContentRootFileProviderプロパティ

コントローラやビューでファイルを参照する場合はIWebHostEnvironmentをDIで解決する感じですね。

// コントローラでファイルを参照する場合
public class SampleController : Controller {
    // wwwrootフォルダのファイルプロバイダ
    private readonly IFileProvider _webRootFileProvider;

    // プロジェクト直下のフォルダのファイルプロバイダ
    private readonly IFileProvider _contentRootFileProvider;

    public SampleController(IWebHostEnvironment env) {
        _webRootFileProvider = env.WebRootFileProvider;
        _contentRootFileProvider = env.ContentRootFileProvider;
    }
}

ASP.NET Core MVC - 時間がかかるアクションメソッドをキャンセルできるようにする

ASP.NET Core 2.2からサポートされているインプロセスホスティングモデル。その特徴の1つをDocsから引用します。

クライアントの切断が検出されます。

クライアントが切断されると、HttpContext.RequestAbortedキャンセルトークンが取り消されます。

docs.microsoft.com

今回はこの機能を使って時間がかかるアクションメソッドのキャンセルを試していきます。

「クライアントの切断」というのはChromeFirefoxの×ボタンやEscキーで発生するようです。ウィンドウやタブの×ボタンではなく、通信中に更新ボタンと同じ位置に現れる×ボタンのことを言っています。

Edgeでも同じことを試してみましたが、どうも発生しない様子でした。

確認した各ブラウザのバージョンです。OSはWindows 10です。

ブラウザのウィンドウやタブを閉じたりとか、XMLHttpRequestの通信なども試していませんがどうなんでしょう。 このあたりの仕様はどこかにあるのかな。(調べずに言ってます。)

すべてのブラウザでキャンセルできるものではないという前提で話を進めていきます。

CancellationTokenをバインドする

アクションをキャンセルできるようにするには、アクションメソッドの引数にCancellationTokenを追加します。CancellationTokenModelBinderクラスがHttpContext.RequestAbortedをバインドしてくれるようです。

後はそのCancellationTokenをチェックするだけです。非同期メソッドの引数に渡していくというか。

public class SampleController : Controller {
    private readonly ILogger _logger;

    public SampleController(ILogger<SampleController> logger) {
        _logger = logger;
    }

    // キャンセルしたいアクション
    // CancellationTokenをバインドする
    public async Task<IActionResult> CancelTest(CancellationToken token) {
        try {
            _logger.LogInformation("Heavy task started.");

            // 何か時間がかかる処理
            // バインドしたCancellationTokenを引数に渡す
            await Task.Delay(TimeSpan.FromSeconds(10), token);

            _logger.LogInformation("Heavy task completed.");
        } catch (TaskCanceledException _) {
            // ブラウザのEscキーでキャンセルされる
            _logger.LogInformation("Heavy task canceled.");
        }

        return new EmptyResult();
    }
}

ChromeFirefoxでアクションのURLにアクセスしてみましょう。

そして途中でEscキーを押すと、アクションメソッドの中のTaskがキャンセルされることを確認できます。

// 出力されるログ
Heavy task started.
Heavy task canceled.

参考

ホスティングモデル自体についてはこのあたりが参考になると思います。

docs.microsoft.com