WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

HttpClient のキャンセルは要求の中断に相当? (CORE)

by WebSurfer 13. July 2021 12:31

下の画像のシステムで、クライアントがブラウザを操作して要求を中断した場合、Web API でのサーバーで実行中の処理をキャンセルできるでしょうか? 自分が検証した限りではできるようです。その詳細を以下に書きます。

システム構成

クライアントがブラウザを使って MVC にアクセスすると、MVC のサーバーは HttpClient クラスを使って Web API にアクセスして必要な情報を取得し、ブラウザに応答として返すというシステムです。

クライアントが要求を送信した後待ちきれなくなって、サーバーによる処理が終わって応答が返ってくる前に要求を中断した場合、それ以上サーバーのリソースを消費しないで済むよう、サーバー側の処理を MVC でも Web API でも中断できるかがポイントです。

なお、クライアントによる要求の中断とは、下の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

要求の中断

先の記事「要求の中断による処理のキャンセル (CORE)」に書きましたように、処理のキャンセルには HttpContext.RequestAborted プロパティで取得できる CancellationToken を利用します。クライアントが要求を中断すると、取得した CancellationToken がキャンセル通知を配信しますので、それをリッスンして処理の中断を行います。

ブラウザ ⇔ MVC の間は、先の記事に書いたように、MVC のアクションメソッドの引数に渡された CancellationToken によるキャンセル通知を利用して MVC のサーバー内での処理を中断できます。

その先の MVC ⇔ Web API の間は MVC のサーバーから HttpClient クラスを利用して Web API にアクセスするというシステムですが、そこがどうできるかをこの記事の下の方に載せた検証用のコードを使って調べてみました。

MVC のアクションメソッドでは次のようにします。HttpClient クラスの SendAsync メソッドや PostAsync メソッドには引数に CancellationToken を取るオーバーロードがあるので、それに HttpContext.RequestAborted プロパティで取得できる CancellationToken を渡します。そうすることで、クライアントによる要求の中断で SendAsync メソッドや PostAsync メソッドの実行をキャンセルできます。

Web API のアクションメソッドの引数にも Web API のサーバー内で HttpContext.RequestAborted プロパティで取得できる CancellationToken を渡します。そのキャンセル通知をリッスンして処理を中断します。

そうした場合、MVC のサーバー内で SendAsync メソッドや PostAsync メソッドの実行がキャンセルされると、Web API のアクションメソッドの引数に渡した CancellationToken はキャンセル通知を配信してくれるかが問題です。

下に載せたコードで検証した結果 Web API の CancellationToken もキャンセル通知を配信してくれることが分かりました。

なので、ブラウザ ⇔ MVC ⇔ Web API という構成でも、適切に CancellationToken を渡してキャンセル通知で処理の中断を行う実装をしておけば、ブラウザで要求を中断しても、MVC でも Web API でもサーバー内の処理を中断できるようです。

参考に検証に使った MVC および Web API のコードを以下に載せておきます。.NET 5.0 の ASP.NET Core アプリで MVC と Web API のプロジェクトは異なります (検証の際のホストが異なりますので、HttpContext.RequestAborted プロパティで取得できる CancellationToken は MVC と Web API で違うものになります)。

MVC(MvcCore5App4)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MvcCore5App4.Models;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;

namespace MvcCore5App4.Controllers
{
    public class HomeController : Controller
    {        
        private readonly ILogger<HomeController> _logger;
        private readonly IHttpClientFactory _clientFactory;

        public HomeController(ILogger<HomeController> logger,
                              IHttpClientFactory clientFactory)
        {
            _logger = logger;
            _clientFactory = clientFactory;
        }

        // ・・・中略・・・

        public async Task<IActionResult> Cancel(CancellationToken token)
        {
            HttpClient client = _clientFactory.CreateClient();
            var url = "https://localhost:44398/api/values";

            // GET 要求する場合はこちら
            //var request = new HttpRequestMessage(HttpMethod.Get, url);
            //HttpResponseMessage response = 
            //                await client.SendAsync(request, token);

            // POST 要求する場合はこちら
            HttpResponseMessage response =
                            await client.PostAsync(url, null, token);

            if (response.IsSuccessStatusCode)
            {
                string result = 
                    await response.Content.ReadAsStringAsync(token);
                ViewBag.Result = result;
            }

            return View();
        }
    }
}

Web API (MvcCore5App2)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using System;

namespace MvcCore5App2.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ILogger<ValuesController> _logger;

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

        [HttpGet]
        public async Task<IActionResult> Get(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return Ok("GET 処理完了");
        }

        [HttpPost]
        public async Task<IActionResult> Post(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return Ok("POST 処理完了");
        }
    }
}

なお、上記は IIS をリバースプロキシに使ってのインプロセス ホスティング モデルに限った話ですので注意してください。

先の記事「要求の中断による処理のキャンセル (CORE)」に書きましたように、アウトプロセス ホスティング モデルや Linux 系の OS で Nginx とか Apache をリバースプロキシに使う場合は CancellationToken のキャンセル通知を配信できませんので、サーバーでの処理の中断はできません。

それから、データベースサーバーを相手にする場合、Entity Framework で使う ToListAsync とか SaveChangesAsync などではどうなるかですが、そこはまだ調べ切れていません。走り出したらキャンセルは効かないということもあるかもしれません。今後の検討課題にしたいと思います。

Tags: , , ,

CORE

要求の中断による処理のキャンセル (CORE)

by WebSurfer 11. July 2021 13:07

IIS をリバースプロキシに使ってのインプロセス ホスティング モデル(この記事の下の方の図参照)でホストされる ASP.NET Core Web アプリは、クライアントによる要求の中断を検出してサーバー側の処理をキャンセルすることができます。(.NET Framework 版の ASP.NET の場合は別の記事「要求の中断による処理のキャンセル (MVC5)」を見てください)

要求の中断

クライアントによる要求の中断とは、上の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

処理のキャンセルには HttpContext.RequestAborted プロパティで取得できる CancellationToken を利用します。クライアントが上に書いた要求の中断操作を行うと、取得した CancellationToken は操作を取り消す通知を配信します。

キャンセル処理は基本的に先の記事「非同期タスクのキャンセル」に書いたことと同様で、以下のようになると思います。

  1. HttpContext.RequestAborted プロパティで CancellationToken を取得しキャンセルをリッスンするタスクに渡す。(CancellationToken の取得先の CancellationTokenSource の初期化等は ASP.NET Core フレームワークがやってくれるようです)  
  2. タスクにはキャンセルをリッスンして適切に処置を行うコードを実装しておく。
  3. クライアントによる要求の中断が検出されると、フレームワークは CancellationTokenSource.Cancel メソッドを呼び出し、CancellationToken を通じてリッスンしているタスクにキャンセルを通知する。
  4. キャンセル通知を受けたタスクは、あらかじめ実装されているコードに従ってキャンセル処置を行う。  

CancellationToken を渡す方法ですが、ネットで見つけた記事 Handling aborted requests in ASP.NET Core に書いてある通り、渡し先のタスクが MVC や Web API のアクションメソッドであれば引数に CancellationToken を追加しておけば、それに HttpContext.RequestAborted から取得した CancellationToken をモデルバインドしてくれます。

検証は Visual Studio 2019 を使って、以下のコードを [デバッグ(D)] ⇒ [デバッグの開始(S)] で IIS 10 Express のインプロセスホスティングで実行して行いました。検証に使用したブラウザは Edge v91.0.864.67, Chrome v91.0.4472.124, Firefox v89.0.2, IE11, Opera v77.0.4054.203 で、いずれもこの記事を書いた時点での最新版です。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MvcCore5App4.Models;
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using System.Threading;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Identity;
using MvcCore5App4.Data;

namespace MvcCore5App4.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

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

        // ・・・中略・・・

        public async Task<IActionResult> Cancel(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return View();
        }
    }
}

上のコードの Task.Delay(5000, token) では渡した token を Deley メソッドの中で継続的に観察しているようで、このメソッドが実行が開始された後でもそれから 5 秒以内ならブラウザで要求を中断すると TaskCanceledException がスローされて処理が終わります。

Entity Framework を使ってデータベースにアクセスして処置を行うときに使う ToListAsync とか SaveChangesAsync などや、HttpClient の SendAsync とか PostAsync なども同様かというのが問題と思いますが、詳しくは調べてなくて分かりません。

ToListAsync(token) メソッドで少し調べてみた限りでは、ToListAsync(token) の次の行で CancellationTokenSource.Cancel() としても完了してしまいました。Task.Delay(5000, token) と違って即終わってしまうので間に合わないのか、走り出したらキャンセルは効かないということなのかは分かりません。

ホスティングモデルによる違いですが、Visual Studio 2019 で IIS 10 Express を使ってのアウトプロセス ホスティング モデルでは要求の中断による処理のキャンセルはできませんでした。

Microsoft のドキュメント「インプロセスおよびアウトプロセス ホスティングの相違点」にインプロセス ホスティングでは "クライアントの切断が検出されます。 クライアントが切断されると、HttpContext.RequestAborted キャンセル トークンが取り消されます" と書いてあります。裏を返すとアウトプロセスホスティングではダメと言っているようです。

構成の違いは以下の図(Microsoft のドキュメントから借用)の通りですが、IIS では切断は検出されるものの Kestrel との間は HTTP 通信なので Kestrel に切断を伝えるすべがないということではないかと思います。ちなみに IIS をリバースプロキシとして使わず Kestrel をエッジサーバーとした場合はインプロセスホスティングと同様に切断は検出されキャンセルも効きます。

インプロセス ホスティング

インプロセス ホスティング モデル

アウトプロセス ホスティング

アウトプロセス ホスティング モデル

IIS を使える環境で Kestrel をエッジに使うことはなさそうですし、Linux 系の OS の場合は Nginx とか Apache をリバースプロキシに使って、Kestrel で ASP.NET Core アプリをホストする、即ち IIS を使ってのアウトプロセスホスティングと同じ構成になるので、 結局は IIS を使ってのインプロセス ホスティング モデルでないとキャンセルはできないということではないかと思います。

Tags: , , , ,

CORE

カスタム Html ヘルパーで IUrlHelper を利用 (CORE)

by WebSurfer 17. April 2021 12:27

先の記事「カスタム Tag ヘルパーで IUrlHelper を利用 (CORE)」のカスタム html ヘルパー版です。加えて、比較のために、同等な機能を部分ビューで実装する方法も書いてみました。

カスタム Html ヘルパー

カスタム html ヘルパーは ASP.NET Core MVC では推奨されてないのか、Microsoft のドキュメントには作成方法の記事が見当たりませんでした。でも、使うことは可能なようですので、先の記事と同じく内部で IUrlHelper を利用する html ヘルパーを書いてみました。

.NET Framework 版の MVC5 アプリでは、カスタム html ヘルパーの中で以下のコードのようにして UrlHelper オブジェクトを取得できます。

using System.Web;
using System.Web.Mvc;

namespace Mvc5App.HtmlHelpers
{
    public static class Mvc5AppHelpers
    {
        public static IHtmlString AchorTag(this HtmlHelper helper,
                                           string contoller, 
                                           string action, 
                                           string text)
        {
            var urlHepler = new UrlHelper(helper.ViewContext.RequestContext);
            var path = urlHepler.Action(action, contoller);

            return MvcHtmlString.Create(
                $"<a href=\"{path}\">{HttpUtility.HtmlEncode(text)}</a>");
        }
    }
}

Core 3.1 / 5.0 版の MVC アプリでは、先の記事「カスタム Tag ヘルパーで IUrlHelper を利用 (CORE)」に書きましたように、ASP.NET Core 組み込みの DI 機能を利用してコンストラクタ経由で IUrlHelperFactory と IActionContextAccessor を DI し、IUrlHelperFactory の GetUrlHelper(ActionContext) メソッドを使って IUrlHelper オブジェクトを取得しました。・・・よく調べてみると、LinkGenerator API を取得して利用する方が良さそうです。LinkGenerator を使ったカスタム html ヘルパーは下の「2021/4/18 追記」に書きます。

しかし、カスタム html ヘルパーで上のコード例のように拡張メソッドを使う場合は、静的クラス内に静的メソッドを配置することになりますので、コンストラクタ経由での DI ができません。

そこをどうするかですが、HttpContextRequestServices プロパティ から IServiceProvider(サービスコンテナーへのアクセスを提供)を取得できますので、それを使って IUrlHelperFactory と IActionContextAccessor のインスタンスを取得できるようです。

HttpContent は IHtmlHelper.ViewContext プロパティから取得できる ViewContext の HttpContext プロパティで取得できます。

それらを利用したカスタム html ヘルパーを書いてみました。以下をコードを見てください。IServiceProvider から IUrlHelperFactory と IActionContextAccessor のインスタンスを取得し、それらを使って IUrlHelper オブジェクトを取得してその Action メソッドにより url パスの文字列を取得しています。(引数の AnchorTagData クラスの定義は先の記事を見てください)

using System;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using MvcCoreApp.Models;

namespace MvcCoreApp.HtmlHelpers
{
    public static class MvcCoreAppHtmlHelpers
    {
        // 静的クラスなのでコンストラクタ経由での DI はできない。
        // IServiceProvider を取得してそれから IServiceCollection に登録されている
        // IUrlHelperFactory と IActionContextAccessor のインスタンスを取得
        public static IHtmlContent AchorTag(this IHtmlHelper helper,
                                            IEnumerable<AnchorTagData> Info)
        {
            IServiceProvider provider = helper.ViewContext.HttpContext.RequestServices;

            var urlFactory = provider.GetRequiredService<IUrlHelperFactory>();

            var actionAccessor = provider.GetRequiredService<IActionContextAccessor>();

            var urlHelper = urlFactory.GetUrlHelper(actionAccessor.ActionContext);

            var content = "";
            foreach (var data in Info)
            {
                var path = urlHelper.Action(
                    action: data.Action,
                    controller: data.Controller);
                content += "<li class=\"nav-item\">" +
                    $"<a class=\"nav-link text-dark\" href=\"{path}\">" +
                    $"{HttpUtility.HtmlEncode(data.Text)}</a>" +
                    "</li>\r\n";
            }

            var output = $"<ul class=\"navbar-nav flex-grow-1\">{content}</ul>";

            return new HtmlString(output);
        }
    }
}

先のカスタム tag ヘルパーの記事と同様に、startup.cs での IActionContextAccessor のサービスへの登録は必要ですので忘れないようにしてください。具体的なコードは先の記事を見てください。

_Layout.cshtml に、上に定義した html ヘルパーが使えるように using 句を記述し、html ヘルパーに渡すモデルを初期化します。さらに、html ヘルパーを表示する場所に @Html.AchorTag(model) というコードを書きます。以下のような感じ。

@using MvcCoreApp.HtmlHelpers;

@{
    IEnumerable<AnchorTagData> model =
        new List<AnchorTagData> {
            new AnchorTagData { Controller="Home", Action="Index", Text="Home" },
            new AnchorTagData { Controller="Home", Action="Privacy", Text="Privacy" },
            new AnchorTagData { Controller="People", Action="Index", Text="People" },
            new AnchorTagData { Controller="Messages", Action="Index", Text="Messages" },
            new AnchorTagData { Controller="Validation", Action="Create", Text="Validation" },
            new AnchorTagData { Controller="Upload", Action="Index", Text="FileUpload" },
            new AnchorTagData { Controller="Products", Action="Index", Text="Products" },
            new AnchorTagData { Controller="Ajax", Action="Index", Text="Ajax" },
            new AnchorTagData { Controller="IHttpClientFactory", Action="Index", Text="HttpClient" }};
}

// ・・・中略・・・

@Html.AchorTag(model)

// ・・・中略・・・

以上により、_Layout.cshtml に書いた @Html.AchorTag(model) というコードの部分に上の画像の赤枠で示したリンクが表示されます。

部分ビューで実装

次に、先の記事のカスタム tag ヘルパー、この記事のカスタム html ヘルパーと同等の機能を部分ビューを使って実装してみます。

ASP.NET Core の組み込みタグヘルパーのアンカータグヘルパーを以下のように部分ビュー _Navi.cshtml(名前は任意)に組み込みます。

@using MvcCoreApp.Models
@using System.Web;
@model IEnumerable<AnchorTagData>

<ul class="navbar-nav flex-grow-1">
    @foreach (var data in Model)
    {
        <li class="nav-item">
            <a class="nav-link text-dark"
               asp-controller=@data.Controller
               asp-action=@data.Action>
                @HttpUtility.HtmlEncode(data.Text)
            </a>
        </li>
    }
</ul>

上記部分ビューを、部分タグヘルパーを使って、以下のように _Layout.cshtml に配置します。

@{
    IEnumerable<AnchorTagData> model = // 省略 (上と同じ)
}

// ・・・中略・・・

<partial name="_Navi.cshtml" model="model" />

// ・・・中略・・・

これだけで、先の記事のカスタム tag ヘルパー、この記事のカスタム html ヘルパーと同様に、上の画像の赤枠で示したリンクが表示されます。この記事で書いた程度のことを実装するなら部分ビューを使うのが一番シンプルでよさそうだと思いました


-------- 2021/4/18 追記 (LinkGenerator 利用) --------

上のカスタム html ヘルパーのコードは、IUrlHelperFactory と IActionContextAccessor をサービスコンテナーから取得し、 IUrlHelperFactory.GetUrlHelper(ActionContext) メソッドを使って IUrlHelper オブジェクトを取得して利用しています。

しかし、後でよく調べたら、Microsoft のドキュメント「URL 生成の概念」に書いてあるように LinkGenerator API を取得して利用する方が良さそうと思いました。

というわけで、LinkGenerator を使ったカスタム html ヘルパーのコードを以下に書きます。IActionContextAccessor のサービスへの登録は不要ですしコードも簡単になります。LinkGenerator はサービスコンテナに登録済みのようで、以下のようにするだけで取得できます。

using System;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Web;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using MvcCoreApp.Models;

// LinkGenarator の利用
using Microsoft.AspNetCore.Routing;

namespace MvcCoreApp.HtmlHelpers
{
    public static class MvcCoreAppHtmlHelpers
    {
        // 静的クラスなのでコンストラクタ経由での DI はできない。
        // IServiceProvider を取得してそれから IServiceCollection に登録されている
        // LinkGenarator のインスタンスを取得して利用する
        public static IHtmlContent AchorTag(this IHtmlHelper helper,
                                            IEnumerable<AnchorTagData> Info)
        {
            IServiceProvider provider = helper.ViewContext.HttpContext.RequestServices;

            var linkGenerator = provider.GetRequiredService<LinkGenerator>();

            var content = "";
            foreach (var data in Info)
            {
                var path = linkGenerator.GetPathByAction(
                    action: data.Action,
                    controller: data.Controller);
                content += "<li class=\"nav-item\">" +
                    $"<a class=\"nav-link text-dark\" href=\"{path}\">" +
                    $"{HttpUtility.HtmlEncode(data.Text)}</a>" +
                    "</li>\r\n";
            }

            var output = $"<ul class=\"navbar-nav flex-grow-1\">{content}</ul>";

            return new HtmlString(output);
        }
    }
}

Tags: , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  August 2021  >>
MoTuWeThFrSaSu
2627282930311
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar