WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core MVC の Bundle と Minify

by WebSurfer 6. February 2020 15:49

ASP.NET Core 3.1 MVC で .css ファイルと .js ファイルをバンドル&ミニファイする機能を実装しようとしてハマった話を書きます。

Bundle と Minify

手順は Microsoft のドキュメント Bundle and minify static assets in ASP.NET Core に詳しく書いてあります。

ドキュメントにはいろいろ書いてありますが、ビルド時にバンドル&ミニファイ版の .css ファイル、.js ファイルを生成するなら下の (1), (2) の手順だけで可能です。

(1) Configure bundling and minification のセクションに従ってアプリケーションルートに bundleconfig.json を追加。以下にドキュメントに記載されている例を書いておきます。詳しい説明はドキュメントを読んでください。

[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  }
]

(2) Build-time execution of bundling and minification のセクションに従って BuildBundlerMinifier を NuGet からインストール。(BundlerMinifier.Core ではないので注意)

BuildBundlerMinifier をインストール

ここまでの設定で、Visual Studio でプロジェクトをビルドする時、bundleconfig.json の inputFiles に指定した .css, .js ファイルをバンドル&ミニファイして outputFileName に指定したパス/ファイル名で配置してくれます。一番上の画像の赤枠で囲ったファイルを見てください。

Visual Studio が自動的にやってくれるのはここまでであることに注意してください。

一番上の画像のようにバンドル&ミニファイ版のファイルを作って配置してくれるだけなので、例えば元のソースが以下のように通常版のパスを参照している場合は site.css ⇒ site.min.css、site.js ⇒ site.min.js に書き換える必要があります。ちなみに、テンプレートで自動生成される _Layput.cshtml がデフォルトで下記のようになっています。

<link rel="stylesheet" href="~/css/site.css" />
<script src="~/js/site.js"></script>

ここが Microsoft のドキュメントに書いてなくて、何故記事の通りやっているのにバンドル&ミニファイされないのか分からず、半日ぐらいハマってしまったところです。(汗)

そんなの当たり前に分かるだろうと思われるかもしれませんね。でも、.NET Framework MVC の場合は、テンプレートでプロジェクトを自動生成するだけで、web.config で <compilation debug="true" ... > と設定してある時は通常版の .css と .js ファイルが、debug="false" の時はバンドル&ミニファイ版が自動的に設定されるのです。

自分は Core MVC でも .NET Framework MVC と同様に、そこまで面倒見てくれると思い込んでいたので気が付きませんでした。(涙)

Tags: , , ,

CORE

ASP.NET Core MVC の ClaimsIdentity

by WebSurfer 4. February 2020 13:56

ASP.NET Core 3.1 Identity で、プロファイル情報を Claim として ClaimsIdentity オブジェクトに追加し、拡張メソッドを使って ClaimsIdentity オブジェクトからプロファイル情報を取得・表示する方法を書きます。

プロファイル情報の表示

.NET Framework ベースの Identity の場合は先の記事「プロファイル情報を ClaimsIdentity へ追加」に書きました。以下は、それと同様な仕組みを ASP.NET Core 3.1 MVC で実装する方法です。

Core 版 Identity にも、.NET Framework 版と同様に、プロファイル情報の一つとして PhoneNumber が IdentityUser クラスに定義済みです。(なので Entity Framework Code First で自動生成される DB のテーブルには PhoneNumber フィールドが含まれます)

先の記事と同様に、ユーザーがログインした際 PhoneNumber を認証クッキーに含めてブラウザに送信し、次の要求を受けた時に認証クッキーから PhoneNumber を取得して上の画像のようにページの右上に表示するコードを実装してみました。

それに何のメリットがあるかと言うと、認証クッキーからプロファイル情報を取得する方が、いちいちデータベースにクエリを投げて取得するより負荷は軽い(であろう)ということです。詳しくは先の記事を見てください。

問題は Visual Studio のテンプレートで自動生成されるコードのどこに Claim を ClaimsIdentity に追加するためのコードを書くかということです。

.NET Framework 版 MVC5 であれば、自動生成される Models/IdentityModels.cs に定義されている ApplicationUser クラスの GenereteUserIdentityAsync メソッドの中に「// ここにカスタム ユーザー クレームを追加します」とコメントが入っていてすぐわかるのですが・・・

という訳で、ネットで asp.net core add custom claim をキーワードにググって調べて、ヒットした以下の記事を参考にさせていただきました:

以下に、定義済みのプロファイル情報 PhoneNumber を Claim として ClaimsIdentity へ追加するコード、ClaimsIdentity からプロファイル情報を取得するための拡張メソッドのコードを載せておきます。上の 2 つの記事のどちらの方法でも OK ですが、下のサンプルコードでは前者の記事の方法を取っています。

ただし、Role を使っている場合は要注意で、上に紹介した記事にある UserClaimsPrincipalFactory<TUser> クラスを使うと Role が働かなくなります(例えばアクションメソッドに [Authorize(Roles ="Administrator")] を付与とすると Administrator ロールを持っているユーザーでもアクセス拒否されます)。

Role が使われている場合は UserClaimsPrincipalFactory<TUser> クラスに代えて、下のサンプルコードのように UserClaimsPrincipalFactory<TUser,TRole> クラスを使ってください。

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Security.Principal;

namespace MvcCoreIdentity.Services
{
  // Role を使う場合 UserClaimsPrincipalFactory<TUser> では
  // なく UserClaimsPrincipalFactory<TUser,TRole> を継承
  public class CustomClaimsPrincipalFactory : 
    UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
  {
    public CustomClaimsPrincipalFactory(
        UserManager<IdentityUser> userManager, 
        RoleManager<IdentityRole> roleManager, 
        IOptions<IdentityOptions> optionsAccessor)
        : base(userManager, roleManager, optionsAccessor)
    {
    }

    // ログイン操作でこのメソッドが呼び出される
    public async override Task<ClaimsPrincipal> CreateAsync(
                                          IdentityUser user)
    {
      var principal = await base.CreateAsync(user);

      // ここでは例として PhoneNumber を Claim として追加。
      // 未登録(DB 上で NULL)の場合 this.PhoneNumber プロ
      // パティは null を返す。null の場合は追加しても意味
      // がないので追加しない
      if (!string.IsNullOrEmpty(user.PhoneNumber))
      {
        ((ClaimsIdentity)principal.Identity).AddClaims(
          new[] { 
            new Claim(ClaimTypes.HomePhone, user.PhoneNumber) 
          });
      }

      return principal;
    }
  }

  // ClaimsIdentity から PhoneNumber を取得する拡張メソッド
  // PhoneNumber が Claims にない場合は null を返す。
  public static class MyExtensions
  {
    public static string GetPhoneNumber(this 
                                        IIdentity identity)
    {
      var claimsIdentity = identity as ClaimsIdentity;
      if (claimsIdentity != null)
      {
        var claim = claimsIdentity.Claims.
                    FirstOrDefault(c => 
                        c.Type == ClaimTypes.HomePhone);
        if (claim != null)
        {
          return claim.Value;
        }
      }
      return null;
    }
  }
}

上のコードの CustomClaimsPrincipalFactory メソッドが動くようにするには Startup.cs の ConfigureServices メソッドで以下のように設定する必要がありますので忘れないようにしてください。

public void ConfigureServices(IServiceCollection services)
{

  // ・・・中略・・・

  // 以下のコードを追加する
  services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
      CustomClaimsPrincipalFactory>();

  services.AddControllersWithViews();
  services.AddRazorPages();
}

上の拡張メソッド GetPhoneNumber は名前空間をインポートすればスコープの中に取り込むことができます。例えば、上の画像のようにマスターページの右上に表示する場合は Views/Shared/_loginPartial.cshtml に以下のように名前空間をインポートし @User.Identity.GetPhoneNumber() というコードを追加します。

@using Microsoft.AspNetCore.Identity
@using MvcCoreIdentity.Services

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" 
           asp-area="Identity" 
           asp-page="/Account/Manage/Index" 
           title="Manage">
            Hello @UserManager.GetUserName(User) / 
            Phone @User.Identity.GetPhoneNumber() !
        </a>
    </li>
・・・以下略・・・

Tags: , , ,

CORE

ASP.NET Core MVC 検証属性の自作

by WebSurfer 3. February 2020 14:52

@IT の連載に「自作の検証属性を定義する(クライアントサイド編)」という .NET Framework MVC アプリにクライアント側で JavaScript による検証を含めて検証属性を自作する記事があります。ASP.NET Core 3.1 MVC ではそれと同等の検証属性をどのように実装するかという話です。

Custom Validator

(注:プロパティにまたがる検証を行うための CustomValidationAttribute の話ではなく、一つのフィールドの内容を独自の条件で検証するためのものです)

.NET Framework MVC との違いは IClientValidatable の代わりに IClientModelValidator インターフェイスを継承し AddValidation メソッドを実装するところです。

Model、検証属性の定義、クライアント側での検証用 JavaScript のコードを以下にアップしておきます。注意点はそれらに書きましたので 見てください。

Model

Model は .NET Framework MVC のものと全く同じです。自作の検証属性 InArrayAttribute は Publish プロパティに付与されています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;

namespace MvcCoreApp.Models
{
  public class Book
  {
    [Key]
    [Display(Name = "ISBNコード")]
    [Required(ErrorMessage = "{0}は必須です。")]
    [RegularExpression(
      "[0-9]{3}-[0-9]{1}-[0-9]{3,5}-[0-9]{3,5}-[0-9A-Z]{1}",
      ErrorMessage = "{0}はISBNの形式で入力してください。")]
    public string Isbn { get; set; }

    [Display(Name = "書名")]
    [Required(ErrorMessage = "{0}は必須です。")]
    [StringLength(100, 
      ErrorMessage = "{0}は{1}文字以内で入力してください。")]
    public string Title { get; set; }

    [Display(Name = "価格")]
    [Range(100, 10000, 
      ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
    public int? Price { get; set; }

    [Display(Name = "出版社")]
    [StringLength(30, 
      ErrorMessage = "{0}は{1}文字以内で入力してください。")]
    [InArray("翔泳社,技術評論社,秀和システム," + 
      "毎日コミュニケーションズ,日経BP社,インプレスジャパン")]
    public string Publish { get; set; }

    [Display(Name = "刊行日")]
    [Required(ErrorMessage = "{0}は必須です。")]
    public DateTime Published { get; set; }
  }
}

自作の検証属性

出版社のリストとの比較検証を行う InArrayAttribute を定義します。IClientModelValidator を継承しているところと、AddValidation メソッドの実装に注目してください。(ちなみに .NET Framework MVC では IClientValidatable インターフェイスの GetClientValidationRules メソッドを使います)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace MvcCoreApp.Models
{
  [AttributeUsage(AttributeTargets.Property, 
                  AllowMultiple = false)]
  public class InArrayAttribute : 
               ValidationAttribute, IClientModelValidator
  {
    // 値リストを表すプライベート変数
    private string _opts;

    // コンストラクタ(値リストとエラーメッセージを設定)
    public InArrayAttribute(string opts)
    {
      this._opts = opts;
      this.ErrorMessage = 
            "{0} は「{1}」のいずれかで指定してください。";
    }

    // プロパティの表示名と値リストでエラーメッセージ作成
    public override string FormatErrorMessage(string name)
    {
      return String.Format(CultureInfo.CurrentCulture, 
                           ErrorMessageString, name, _opts);
    }

    // サーバー側での検証
    // 値リストに入力値が含まれているかをチェック
    public override bool IsValid(object value)
    {
      // 入力値が空の場合は検証をスキップ 
      if (value == null) { return true; }

      // カンマ区切りテキストを分解し、入力値valueと比較 
      if (Array.IndexOf(_opts.Split(','), value) == -1)
      {
        return false;
      }
      return true;
    }

    // IClientModelValidator が実装するメソッド。
    // 検証対象の html 要素 (input) に控えめな JavaScript
    // による検証のための属性 (data-val 他) と値を追加
    public void AddValidation(
            ClientModelValidationContext context)
    {
      MergeAttribute(context.Attributes, "data-val", "true");
      var errorMessage = FormatErrorMessage(
          context.ModelMetadata.GetDisplayName());
      MergeAttribute(context.Attributes, 
                     "data-val-inarray", errorMessage);
      MergeAttribute(context.Attributes, 
                     "data-val-inarray-opts", this._opts);
    }

    // 上の AddValidation メソッドで使うヘルパーメソッド
    private bool MergeAttribute(
                  IDictionary<string, string> attributes, 
                  string key, string value)
    {
      if (attributes.ContainsKey(key))
      {
        return false;
      }
      attributes.Add(key, value);
      return true;
    }
  }
}

検証用 JavaScript(View に組み込み)

View にインラインで書いていますがコード自体は @IT の記事のものと同じです。検証用 JavaScript ライブラリに自作の検証スクリプトをどのように追加し、連携を取って動くようにするかという点がキモです。後者については、Unobtrusive Client Validation in ASP.NET MVC 3 という記事の中の Single value validators というセクションに記述がありました。正直読んでもよく分からなかったですが。(汗)

@model MvcCoreApp.Models.Book

・・・中略・・・

<div class="form-group">
  <label asp-for="Publish" class="control-label"></label>
  <input asp-for="Publish" class="form-control" />
  <span asp-validation-for="Publish" class="text-danger">
  </span>
</div>

・・・中略・・・

@section Scripts {
  @{
    await 
    Html.RenderPartialAsync("_ValidationScriptsPartial");
  }

  <script type="text/javascript">
    $.validator.addMethod("inarray",
      function (value, element, parameters) {
        // 入力値が空の場合は検証をスキップ
        value = $.trim(value);
        if (value === '') {
          return true;
        }

        // カンマ区切りテキストを分解し、入力値valueと比較
        if ($.inArray(value, parameters.split(',')) === -1) {
          return false;
        }
        return true;
      });

    $.validator.unobtrusive.adapters.
                addSingleVal('inarray', 'opts');
  </script>
}

Tags: , , ,

CORE

About this blog

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

Calendar

<<  February 2020  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
2425262728291
2345678

View posts in large calendar