WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core MVC で三点リーダー表示

by WebSurfer 19. May 2024 13:21

ASP.NET Core MVC で表示するテーブルで css の overflow: hidden と text-overflow: ellipsis を使って文字列の長さ制限するとともに末尾に三点リーダーを表示する例を紹介します。下の画像がその例です。ターゲットフレームワークは .NET 8.0、ブラウザは Chrome、フォントはメイリオ、サイズは 16px です。

文字列の長さ制限、三点リーダー表示

先の記事「文字列の長さ制限、三点リーダー表示」では、ASP.NET Web Forms アプリの GridView の例を紹介しました。

その記事と同様に、description 列の文字列を div 要素に入れて、それに以下の css を適用しています。

<style type="text/css">
    div.style1
    {
        width: 320px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
</style>

html と css の機能なのでアプリが ASP.NET Web Forms から ASP.NET Core MVC に変わっても関係ないと思われるかもしれませんが、ブラウザに送信される文字の形式が異なります。結果は同じになりますが。

参考までに以下に違いを書いておきます。

先の記事の Web Forms の例では、コードビハインドで C# のコードで設定した文字列は UTF-8 に変換されてブラウザに送信されています。文字列は Literal コントロールの Text プロパティに設定したのでエスケープはされません。(なので、先の記事では < > & という文字は C# のコードでは &lt; &gt; &amp; としています)

一方、この記事の ASP.NET Core MVC の場合は、< > & という文字は ASP.NET がエスケープして &lt; &gt; &amp; という文字列としてブラウザに送信されます。また、日本語や絵文字(たぶん非 ASCII 文字すべて)は数値文字参照 &#xNNNN; 形式(NNNN は 16 進 Unicode)に変換されてブラウザに送信されます。以下の画像を見てください。

html ソース

ちなみに、例えば、🍎 は上の画像では &#x1F34E; に該当します。🍎 を IME パッドで見ると Unicode: U+1F34E となっています。

🍎 のコード

一番上の画像の結果から分かるように、サーバーからブラウザに送信される各文字の形式は関係なく、ブラウザ上に表示された文字列の長さで制限がかかり、css の width: 320px で指定された幅いっぱいに三点リーダを含めて表示されています。

フォントはメイリオ、サイズは 16px ですが、MS Gothic などの等幅フォントを使った場合も、フォントサイズを変えた場合も、ブラウザ上に表示される文字列の長さで制限がかかるのは同じです。

三点リーダーを表示する text-overflow:ellipsis はもともと IE の独自拡張だそうですが、最近は他のブラウザでも取り入れられているようです。Windows 10 の Chrome 125.0.6422.61, Edge 125.0.2535.51, Firefox 126.0, Opera 110.0.5130.23 で試してみましたが、同じ結果が得られました。

参考に、上の画像を表示するのに使った ASP.NET Core MVC アプリのコードを載せておきます。

Model

namespace MvcNet8App2.Models
{
    public class Ellipsis
    {
        public int Id { get; set; }
        public string Description { get; set; } = null!;
    }
}

View

@model IEnumerable<MvcNet8App2.Models.Ellipsis>

@{
    ViewData["Title"] = "Ellipsis";
}

<style type="text/css">
    body {
        font-family: "メイリオ";
        font-size: 16px;
    }

    div.style1 {
        width: 320px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
</style>

<h1>Ellipsis</h1>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Id)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Description)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Id)
                </td>
                <td>
                    <div class="style1">
                        @Html.DisplayFor(modelItem => item.Description)
                    </div>
                </td>                
            </tr>
        }
    </tbody>
</table>

Controller / Action Method

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MvcNet8App2.Models;
using System.Diagnostics;
using System.Security.Claims;
using System.Security.Principal;

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

        public HomeController(ILogger<HomeController> logger,
                              IOptions<CookieAuthenticationOptions> options)
        {
            _logger = logger;
            _options = options.Value;
        }


        // ・・・中略・・・

        public IActionResult Ellipsis()
        {
            var model = new List<Ellipsis>
            {
                new ()
                {
                    Id = 1,
                    Description = "エスケープされた < > & " +
                    "などの文字はどのようになるか?"
                },
                new() {
                    Id = 2,
                    Description = "Proportional Font WWWWWWWWWWW " +
                    "iiiiiiii llllll などは?"
                },
                new ()
                {
                    Id = 3,
                    Description = "サロゲートペア 𠀋 𡈽 𠮟 などは" +
                    "どのようになるか?"
                },
                new() {
                    Id = 4,
                    Description = "絵文字 🍎 🍏 (サロゲートペア) " +
                    "👨‍🌾 (ZWJ で結合) などは?"
                }
            };

            return View(model);
        }

        // ・・・中略・・・
    }
}

Tags: , , , , ,

CORE

ASP.NET Core でのデータ保護キーの管理

by WebSurfer 25. April 2024 19:11

ASP.NET Core Web アプリを IIS でホストするためにアプリケーションプールを作成する際、設定によってはデータ保護キーがリサイクルで失われ、ブラウザから有効な認証クッキー/チケットが送られてきても復号できないので認証に失敗するという話を書きます。元の話は「ASP.NET COREで、Cookie認証が維持できない」です。

復号できず認証失敗

上の画像は認証チケットが期限切れか否かを調べるために自分が作ったページで (コードは下に載せます)、データ保護キーがリサイクルで失われたため認証クッキーを復号できず、例外がスローされた結果です。

ASP.NET Core アプリのデータ保護キーの管理は、Microsoft のドキュメント「ASP.NET Core でのデータ保護のキー管理と有効期間」によると、アプリにより運用環境が検出され、以下のいずれかになるそうです。

  1. アプリが Azure Apps でホストされている場合、キーは %HOME%ASP.NET_DataProtection-Keys フォルダーに保持されます。
  2. ユーザー プロファイルを使用できる場合、キーは %LOCALAPPDATA%\ASP.NET\DataProtection-Keys フォルダーに保持されます。
  3. アプリが IIS でホストされている場合、HKLM レジストリ内の、ワーカー プロセス アカウントにのみ ACL が設定されている特別なレジストリ キーにキーが保持されます。
  4. これらの条件のいずれにも該当しない場合、キーは現在のプロセスの外部には保持されません。 プロセスがシャットダウンすると、生成されたキーはすべて失われます。

(ご参考までに、.NET Framework 版の ASP.NET Web アプリの場合は web.config に配置する machineKey 要素 (ASP.NET 設定スキーマ) に従ったデータ保護キーの管理を行い、デフォルト AutoGenerate, IsolateApps の場合はレジストリにデータ保護キーが保持されます)

ということで、IIS でホストされる場合は上の 2 と 3 が関係することになります。ApplicationPoolIdentity にユーザープロファイルは無いので 2 は関係ないと思っていたのですが、そうではなかったです。以下に 2, 3 それぞれの注意点を述べます。

2. ユーザープロファイルを使用

IIS でホストされた ASP.NET Core アプリを動かすとアプリケーションプール名で c:\users フォルダにフォルダが生成され、上の 2 で言う「ユーザー プロファイルを使用できる」状態になります。

%LOCALAPPDATA% は c:\users\<アプリケーションプール名>\AppData\Local となります。(注: アプリケーションプール名の新規ユーザーが Windows に追加されるわけではなくてフォルダが生成されるだけです)

ただし、フォルダが作成されても、applicationHost.config で setProfileEnvironment 属性が true になってないとその下に ASP.NET\DataProtection-Keys ホルダは作られず、データ保護キーも生成されません。

さらに、[ユーザープロファイルの読み込み]を Ture に設定する必要があります。[ユーザープロファイルの読み込み]が False に設定されていると、データ保護キーが DataProtection-Keys フォルダに存在していても、リサイクル後は認証に失敗します。メモリに保持したデータ保護キーを使うようです。この状態で 3 の条件のレジストリにもキーがある場合はどうなるか未検証です。

上に紹介した Stackoverflow のスレッドの話では、質問者さんの Windows Server 2019 の環境では setProfileEnvironment 属性が true になっていたそうですが、自分の Windows 10 Pro 環境では false になっていました。デフォルトで true だそうですが、applicationHost.config を開いて調べた方が良さそうです。

3. レジストリを使用

アプリケーションプールを作成する際、 [.NET CLR バージョン] を [マネージド コードなし] にするとデータ保護キーはレジストリに保持されることはなく、上の 2 つ目の条件で ASP.NET\DataProtection-Keys ホルダにデータ保護キーが保持されてない限り、リサイクル後はデータ保護キー失われ、有効な認証クッキー・チケットがブラウザから送られてきても認証に失敗します。

解決策は[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしておく、もしくは、一度ホストする ASP.NET Core アプリを[.Net CLR バージョン v4.0.30319]で動かした後で、 [マネージド コードなし] に設定することです。(注: いろいろ試した結果から分かったことで Microsoft の公式ドキュメントに書いてあることではありません)

.NET CLR バージョンの設定

とにかく一度[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしてアプリを動かせば、レジストリにデータ保護キーが保持されるようなって、リサイクルでデータ保護キーが失われることはなくなるようです。理由は不明です。

なお、アプリケーションプールの名前はデータ保護キーの保存先のレジストリと関連付けられるようですので注意してください。そして、そのレジストリはアプリケーションプールを削除しても残るようです。レジストリにデータ保護キーを保存できるように設定したアプリケーションプールを一旦削除してから、同じ名前で [.NET CLR バージョン] を [マネージド コードなし] にしてアプリケーションプールを作成しても、リサイクル後の認証は維持できました。

以上、2, 3 それぞれの注意点です。思うに、3 のレジストリの使用に頼らないで、確実に 2 のユーザープロファイルのフォルダのデータ保護キーを使用できるよう設定するのが本筋かもしれません。

なぜなら、Microsoft のドキュメント「ASP.NET Core モジュールと IIS の詳細な構成」の「IIS サイトを作成する」のセクションで、 [マネージド コードなし] に設定することが推奨されていますから。


以下にどのように検証したかを備忘録として残しておきます。環境は上にも書きましたが、自分の Windows 10 Pro の開発マシンのローカル IIS 10.0 です。自分の Windows 10 Pro では applicationHost.config の setProfileEnvironment 属性が false になっており、以下の (8) まではその状態のままですので注意してください。

検証用のアプリは Microsoft のドキュメント「ASP.NET Core Identity を使用せずに cookie 認証を使用する」から入手した .NET 6.0 の Razor Pages アプリに少し手を加えたものです。

(1) C:\WebSites2019 というフォルダ下に AspNetCoreCookieAuth2 という名前のフォルダを作成。

(2) IIS Manager を起動して AspNetCoreCookieAuthTest という名前でアプリケーションプールを新たに作成。その際[アプリケーション プールの編集]ウィンドウで[.NET CLR バージョン]を[マネージド コードなし]に設定。その他はデフォルトのままとしておきます。結果は以下の画像の通りです。

アプリケーションプール詳細

(3) IIS Manager のサイトの追加で、上記 (1) のフォルダをサイトに設定。サイト名は AspNetCoreCookieAuth2 とし、アプリケーションプールは上記 (2) で新たに作成したものを設定。

サイトの設定

サイトバインド設定は、種類: http, IP アドレス: 未使用の IP アドレスすべて, ポート: 80, ホスト名: www.aspnetcorecookieauth2.com とします。

サイトバインド

(4) hosts ファイルに 127.0.0.1 www.aspnetcorecookieauth2.com を追加します。

(5) ダウンロードした .NET 6.0 のサンプル Razor Pages プロジェクトを Visual Studio 2022 で開いて以下のように手を加えます。

  • 永続化(認証クッキーを HDD/SSD に保存)するため既存の Login.cshtml.cs の IsPersistent = true, のコメントアウトを解除
  • 認証チケットの有効期限を 1 週間に設定するため Program.cs の .AddCookie のオプションで有効期限を指定
  • 暗号化されている認証チケットを復号し、認証チケットの有効期限の日時を取得するページ AuthExpireCheck を追加。cshtml.cs のコードは以下の通りです(cshtml のコードは省略します)
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Principal;

namespace CookieSample.Pages
{
    public class AuthExpireCheckModel : PageModel
    {
        private readonly CookieAuthenticationOptions _options;

        public AuthExpireCheckModel(IOptions<CookieAuthenticationOptions> options)
        {
            _options = options.Value;
        }

        public string Message { get; set; }

        public void OnGet()
        {
            try
            {
                // デフォルトのクッキー名は ASP.NET Core Identity の
                // .AspNetCore.Identity.Application とは異なり
                // .AspNetCore.Cookies となっているので注意
                string cookie = Request.Cookies[".AspNetCore.Cookies"];
                if (!string.IsNullOrEmpty(cookie))
                {
                    IDataProtectionProvider provider = _options.DataProtectionProvider;
                    IDataProtector protector = provider.CreateProtector(
                        // CookieAuthenticationMiddleware のフルネーム
                        "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                        // クッキー名 .AspNetCore.Cookies から .AspNetCore.
                        // を削除した文字列
                        "Cookies",
                        // .NET Framework 版は "v1"、Core 版は "v2"
                        "v2");

                    // 認証クッキーから暗号化された認証チケットを復号
                    TicketDataFormat format = new TicketDataFormat(protector);
                    AuthenticationTicket authTicket = format.Unprotect(cookie);

                    // ユーザー名を取得
                    ClaimsPrincipal principal = authTicket.Principal;
                    IIdentity identity = principal.Identity;
                    string userName = identity.Name;

                    // 認証チケットの有効期限の日時を取得
                    AuthenticationProperties property = authTicket.Properties;
                    DateTimeOffset? expiersUtc = property.ExpiresUtc;

                    // ExpiresUtc と現在の時刻を比較して期限切れか否かを判定
                    if (expiersUtc.Value < DateTimeOffset.UtcNow)
                    {
                        Message = $"{userName} 認証チケット期限切れ {expiersUtc.Value}";
                    }
                    else
                    {
                        Message = $"{userName} 認証チケット期限内 {expiersUtc.Value}";
                    }
                }
                else
                {
                    Message = "認証クッキーがサーバーに送信されてきていません。";
                }
            }
            catch (CryptographicException ex)
            {
                Message = $"CryptographicException: {ex.Message}";
            }
            catch (Exception ex)
            {
                Message = $"Exception: {ex.Message}";
            }
        }
    }
}

(6) Microsoft のチュートリアル「IIS に ASP.NET Core アプリを発行する」の「アプリを発行および配置する」セクションに従って、上のステップで作成した C:\WebSites2019\AspNetCoreCookieAuth2 フォルダにプロジェクトを発行。インプロセスホスティングになります (Microsoft 推奨)。

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

(7) ブラウザからサイトにアクセスしてログインしてから上の (5) で追加した AuthExpireCheck ページを要求すると以下の通りとなります。一旦ブラウザを閉じて、再度立ち上げ AuthExpireCheck ページを要求しても認証は維持されます。(IsPersistent = true としたことによる Set-Cookie の expires 属性は有効に機能していることが確認できる)

認証は維持されている

(8) IIS Manager を操作してアプリケーションプルをリサイクルし、ブラウザから AuthExpireCheck ページを要求するとこの記事の一番上の画像の結果となります。ブラウザからは上の (7) の操作で入手した認証クッキーをサーバーに送信していますが、リサイクルによってデータ保護キーが失われているので復号できず例外がスローされています。

(9) 自分の Windows 10 Pro 環境では applicationHost.config の setProfileEnvironment 属性は以下の通り false になっています。(Windows Server 2019 では true になっているそうです)

<applicationPoolDefaults managedRuntimeVersion="v4.0">
  <processModel 
    identityType="ApplicationPoolIdentity" 
    loadUserProfile="true" 
    setProfileEnvironment="false" />
</applicationPoolDefaults>

なので、C:\Users\AspNetCoreCookieAuthTest\AppData\Local には ASP.NET\DataProtection-Keys ホルダは作らておれず、データ保護キーも生成されていません。

%LOCALAPPDATA%

setProfileEnvironment を ture に設定し、アプリケーションプール AspNetCoreCookieAuthTest を再起動してから、ASP.NET Core アプリを呼び出すと ASP.NET\DataProtection-Keys フォルダが作られ、その中にデータ保護キーが生成されました。

生成されたデータ保護キー

(10) その後であれば、上の (2) の画像のように[ユーザープロファイルの読み込み]が Ture に設定してあれば、データ保護キーは ASP.NET\DataProtection-Keys フォルダから取得され、リサイクル後も認証は維持されます。

(11) [ユーザープロファイルの読み込み]を False に設定して試してみましたが (Windows Server 2019 では False がデフォルトとのことです)、データ保護キーが DataProtection-Keys フォルダに存在していても、リサイクル後は認証に失敗します。メモリに保持したデータ保護キーを使うようです。

Tags: , , , , ,

CORE

Razor クラスライブラリ

by WebSurfer 20. March 2024 15:43

Razor Class Library (RCL) に ASP.NET Core MVC の Controller と View を実装し、それを ASP.NET Core MVC アプリから利用する例を書きます。

Razor クラスライブラリの利用

話の発端は Microsoft Q&A のスレッド「.NET MVCで参照登録したDLLのビューを使用する方法」でクラスライブラリ中のビューが見つからないという話です。それに対処する方法を検討した時の話を備忘録として残しておくことにしました。

ASP.NET Core MVC の cshtml ファイルは、Microsoft のドキュメント「ASP.NET Core での Razor ファイルのコンパイル」によるとデフォルトでビルド時及びデプロイ時に dll にコンパイルされるそうなので、.NET Framework 版の MVC のようなランタイムコンパイルだからダメということはなさそうです。

ただし、アクションがビューを返すときにビューの検出と呼ばれるプロセスが行われるそうで、Microsoft のドキュメント「ビューの検出ルール」に書いてあるように、既定のフォルダに当該ビューが存在しなければなりません。

Microsoft Q&A の話は、独立したクラスライブラリに MVC の Controller と View を実装した結果、MVC アプリがビューを見つけることができず、InvalidOperationException: The view 'Index' was not found いう例外がスローされたということです。

クラスライブラリではなくて、ASP.NET Core MVC 本体のプロジェクトに Controller, Model, View を実装すればそういう悩みは解消できますが、どうしても独立したライブラリを使わなければならない事情があるなら、Razor Class Library として作るのが良さそうです。

Razor Class Library は Razor Pages だけでなく MVC もサポートしており、設定によって Controller, View, Model を実装して、MVC アプリからそれらを呼び出すことができるそうです。

ということで、Microsoft のドキュメント「ASP.NET Core の Razor クラス ライブラリ プロジェクトを使用した再利用可能 UI の作成」とネットで見つけた記事「Working with Razor Class Libraries in ASP.NET Core」を参考にして作ってみました。

ソリューションの構成

上の画像の RazorClassLib が RCL プロジェクトで、RazorClassLibraryHost が MVC アプリのプロジェクトです。両方とも Visual Studio 2022 のテンプレートで作って、前者には Controllers, Models, Views フォルダを追加し、それらの中に Controller, Model, View クラスを実装しています。後者はテンプレートで作ったまま何も手を加えてません。

MVC アプリの実行結果がこの記事の一番上の画像で、MVC アプリから RCL の Controller / View を呼び出して結果を表示したものです。ビューが見つからないという問題は起こりません。

何も難しいことは無かったのですが、Visual Studio で RCL プロジェクトを作成する際に注意すべき点があって、それは[サポート ページとビュー]にチェックを入れることです。

[サポート ページとビュー]にチェック

上に紹介した Microsoft のドキュメントに "Select Support pages and views if you need to support views. By default, only Razor Pages are supported." と書いてありますが、デフォルト([サポート ページとビュー]にチェック無し)では Razor Pages も MVC もサポートされません。その下に書いてある "The Razor class library (RCL) template defaults to Razor component development by default. The Support pages and views option supports pages and views." が正しいです。

Microsoft のドキュメント「クラス ライブラリで ASP.NET Core API を使用する」の「MVC 拡張機能を含める」のセクションの説明によると以下の設定が必要とのことです。

  1. Microsoft.NET.Sdk.Razor SDK を使用する。
  2. true に設定された AddRazorSupportForMvc MSBuild プロパティ。
  3. 共有フレームワークの <FrameworkReference> 要素。

Visual Studio 2022 の RCL テンプレートで[サポート ページとビュー]にチェックを入れて作成したプロジェクトは上の要件を満たしていて、生成されるプロジェクトファイルの内容は以下のようになります。

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
  
</Project>

その他、気が付いた点を以下に書いておきます。

MVC アプリから RCL に参照設定がされていれば、RCL の Controller は、MVC アプリのプロジェクトの Program.cs に含まれる builder.Services.AddControllersWithViews(); で DI コンテナに登録されるようです。なので、参照設定以外は何もしなくても、要求に応じて RCL の Controller が呼び出され応答が返ってきます。

MVC アプリから RCL の Razor Pages を呼び出す場合は、Program.cs に builder.Services.AddRazorPages(); と app.MapRazorPages(); を追加する必要があります。追加しないと見つからないというエラーになります。

MVC アプリに実装されている _Layout.cshtml が自動的に使われます。なので、MVC アプリの wwwroot に実装されている Bootstrap などの CSS や jQuery などの JavaScript も有効になります。

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2024  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar