ASP.NET Core. 3.1 MVC アプリでファイルをアップロードする際に、カスタム検証属性を利用してファイルのサイズとタイプをクライアント側とサーバー側の両方で検証し、検証結果 NG の場合はエラーメッセージを表示する方法について書きます。
基本的には先の記事「ASP.NET Core MVC 検証属性の自作」の応用です。先の記事ではパラメータが 1 つだけでしたが、この記事ではファイルのサイズとタイプという 2 つを検証するところが大きな違いです。
クライアント側での検証は、Web Forms アプリの記事「FileUpload と CustomValidator」と同様に、ブラウザの HTML5 File API を利用して JavaScript でファイルのサイズとタイプ情報を取得し、外部から入力されるパラメータ値と比較して検証します。
そのパラメータ値はカスタム検証属性(下のコード例では FileValidationAttribute)をモデルのプロパティに設定する際に引数(下のコード例では 50000 と "image/jpeg")として渡すようにしています。
サーバー側では引数に渡されたパラメータ値(ファイルサイズ / タイプ)を容易に取得でき、検証属性クラスの IsValid メソッドに引数として渡される IFromFile オブジェクトから取得できるファイルサイズとタイプを比較して検証できます。問題はクライアント側での検証用 JavaScript / jQuery にそのパラメータ値をどのように渡すかです。
パラメータが 1 つだけなら先の記事「ASP.NET Core MVC 検証属性の自作」で書いた View の中のスクリプトのように addSingleVal メソッドを利用できます。しかし、2 つあると addSingleVal メソッドでは何ともならず、add メソッドを利用せざるを得ないようです。
add メソッドの使い方は、先の記事を書くときに調べた Unobtrusive Client Validation in ASP.NET MVC 3 という記事に記述がありましたが、具体的にどういうコードを書けばいいのかが分かりません。
やむを得ず検証用の控えめな JavaScript のソースコード jquery.validate.unobtrusive.js を調べると、addBool, addSingleVal, addMinMax メソッドは add メソッドをラップしたものだということが分かりました。その中の addMinMax メソッドが min, max というパラメータを 2 つ渡していますので、それを見よう見まねで書いてみたのが下の View の中のコードです。
Edge, Chrome のデバッガでスクリプトをステップ実行するなどして調べましたが一応は期待通り動いていることを確認しました。具体的にどのような仕組みで動いているかまでは分かっていませんが。(汗)
以下にサンプルコードをアップしておきます。上の画像を表示したものです。要点はコメントに書いたつもりですのでそれを見てください。(手抜きでスミマセン)
Model とカスタム検証属性
using System;
using System.Collections.Generic;
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;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MvcCoreApp.Models
{
// Model
public class FileUpload
{
[Required(ErrorMessage = "{0} is required.")]
public string Name { get; set; }
[Display(Name = "Upload file")]
[Required(ErrorMessage = "{0} is required.")]
[FileValidation(50000, "image/jpeg")]
public IFormFile PostedFile { get; set; }
}
// カスタム検証属性
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class FileValidationAttribute : ValidationAttribute,
IClientModelValidator
{
private readonly long size;
private readonly string type;
public FileValidationAttribute(long size, string type)
{
this.size = size;
this.type = type;
this.ErrorMessage = "{0} must be less than {1} byte, {2}";
}
public override string FormatErrorMessage(string displayName)
{
return String.Format(CultureInfo.CurrentCulture,
ErrorMessageString, displayName,
this.size, this.type);
}
public override bool IsValid(object value)
{
var postedFile = value as IFormFile;
if (postedFile == null || postedFile.Length == 0)
{
return false;
}
if (postedFile.Length <= this.size &&
postedFile.ContentType == this.type)
{
return true;
}
return false;
}
// IClientModelValidator が実装するメソッド。検証対象の input
// 要素に控えめな JavaScript (jquery.validate.unobtrusive.js)
// による検証のための属性と値を追加
public void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(
context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes,
"data-val-filevalidation", errorMessage);
MergeAttribute(context.Attributes,
"data-val-filevalidation-size", this.size.ToString());
MergeAttribute(context.Attributes,
"data-val-filevalidation-type", this.type);
}
// 上の AddValidation メソッドで使うヘルパーメソッド
private bool MergeAttribute(IDictionary<string, string> attributes,
string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
}
}
View
@model MvcCoreApp.Models.FileUpload
@{
ViewData["Title"] = "Upload";
}
<h1>Upload</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Upload" enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PostedFile" class="control-label"></label>
<input type="file" asp-for="PostedFile" />
<br />
<span asp-validation-for="PostedFile" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
//<![CDATA[
$.validator.addMethod("filevalidation",
function (value, element, parameters){
// HTML5 File API の File, FileList がサポートされている
// 場合のみ検証する。サポートされてないとスクリプトエラ
// ーになるので下の if 文は必要
if (window.File && window.FileList) {
// ファイルが選択されてないと fileUpload.files[0] は
// undefined になる。element.files[0] == null は念のため
if (element.files[0] == undefined ||
element.files[0] == null) {
// ファイル未選択は RequiredAttribute で検証する
// ことにし、ここでは true を返すことにした(検証
// とエラーメッセージがダブらないように)
return true;
}
if (element.files[0].type != parameters.type) {
return false;
}
if (element.files[0].size > Number(parameters.size)) {
return false;
}
return ture;
} else {
// HTML5 File API の File, FileList がサポートされて
// いない場合はクライアント側では検証しない
return ture;
}
});
// パラメータを 2 つ渡す方法が問題。add メソッドを使わざるを得ない
$.validator.unobtrusive.adapters.
add("filevalidation", ["type", "size"], function (options) {
// 上の検証スクリプトで parameters.type, parameters.size と
// という形でパラメータを取得するには以下のように JavaScript
// オブジェクトとして setValidationValues の第 3 引数に渡す。
// 配列 [options.params.size,options.params.type] としても渡
// せるが、取得するとき parameters[0] のようにする必要がある
var value = {
size: options.params.size,
type: options.params.type
};
setValidationValues(options, "filevalidation", value);
});
// jquery.validate.unobtrusive.js からコピーしたヘルパーメソッド
function setValidationValues(options, ruleName, value) {
options.rules[ruleName] = value;
if (options.message) {
options.messages[ruleName] = options.message;
}
}
//]]>
</script>
}
Comtroller / Action Method
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Http;
using System.IO;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MvcCoreApp.Controllers
{
public class ValidationController : Controller
{
public IActionResult Upload()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Upload(FileUpload model)
{
string result = "";
IFormFile postedFile = model.PostedFile;
if (ModelState.IsValid)
{
string filename = Path.GetFileName(postedFile.FileName);
result = filename + " (" + postedFile.ContentType + ") - " +
postedFile.Length + " bytes アップロード完了";
}
else
{
result = "ファイルアップロードに失敗しました";
}
ViewBag.Result = result;
return View();
}
}
}