はじめに
近年の .NET開発では、サービスなどの依存性はDIによって注入するのが一般的です。
そして、アノテーションベースの検証で独自のチェック処理を実現する ValidationAttribute
を継承したクラスにおいても、DIによって外部サービスの依存性を注入します。
ここで問題が…
通常、ValidationAttribute
を継承したバリデーションで、外部のサービスをDIする場合は、オーバーライドしたIsValid
メソッドの引数であるvalidationContext
のGetService
メソッドを使います。
しかし、Blazorのアプリケーションから、このカスタムバリデーションを呼び出すとGetService
メソッドも戻り値がnull
になります。
ちなみに、他のASP .NET Core MVCなどのアプリケーションでは、このコードで正常に外部サービスが取得できます。
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) {
var serivce = validationContext.GetService<UserService>();
//なぜか「serivce」がnullになる
}
問題の原因は「DataAnnotationsValidator」コンポーネント
この問題の解決方法が、Stack Overflowの以下の投稿で紹介されている。
How to get/inject services in custom ValidationAttributes
原因をざっくり言うと、Blazorでフォーム検証を実現する<DataAnnotationsValidator/>
コンポーネントの初期化時に、依存関係の解決に必要なServiceProvider
の指定がされていないため、GetService
の結果がすべてnull
になっているようだ。
じゃあ、<DataAnnotationsValidator/>
を直せば?という話であるが、これは .NET のコンポーネントであるため、ソースはいじれない。
解決方法としては、次に紹介するDataAnnotationsValidator
をベースとした独自のバリデータークラスを作成する方法がある。
BlazorのカスタムバリデーションでDIを実現する方法
前述の問題を回避し、カスタムバリデーションのクラスからDIを実現方法を紹介する。
まず .NET標準のDataAnnotationsValidator
のソースコードをベースに、独自のCustomValidator
というクラスを作る。
How to get/inject services in custom ValidationAttributes
上のリンクにも掲載されているコードであるが、この記事にも掲載しておく。
using System.Reflection;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
/// <summary>
/// blazorの入力検証で呼ばれるデータアノテーションからDI(インジェクション)するためのカスタムバリデーター
/// </summary>
public class CustomValidator : ComponentBase, IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> PropertyInfoCache = new ConcurrentDictionary<(Type, string), PropertyInfo>();
[CascadingParameter] EditContext CurrentEditContext { get; set; }
[Inject] private IServiceProvider serviceProvider { get; set; }
private ValidationMessageStore messages;
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(CustomValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(CustomValidator)} " + "inside an EditForm.");
}
this.messages = new ValidationMessageStore(CurrentEditContext);
CurrentEditContext.OnValidationRequested += validateModel;
CurrentEditContext.OnFieldChanged += validateField;
}
private void validateModel(object sender, ValidationRequestedEventArgs e)
{
var editContext = (EditContext) sender;
var validationContext = new ValidationContext(editContext.Model);
validationContext.InitializeServiceProvider(type => this.serviceProvider.GetService(type));
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
messages.Clear();
foreach (var validationResult in validationResults)
{
if (!validationResult.MemberNames.Any())
{
messages.Add(new FieldIdentifier(editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage);
continue;
}
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
private void validateField(object? sender, FieldChangedEventArgs e)
{
if (!TryGetValidatableProperty(e.FieldIdentifier, out var propertyInfo)) return;
var propertyValue = propertyInfo.GetValue(e.FieldIdentifier.Model);
var validationContext = new ValidationContext(CurrentEditContext.Model) {MemberName = propertyInfo.Name};
validationContext.InitializeServiceProvider(type => this.serviceProvider.GetService(type));
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
messages.Clear(e.FieldIdentifier);
messages.Add(e.FieldIdentifier, results.Select(result => result.ErrorMessage));
CurrentEditContext.NotifyValidationStateChanged();
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (PropertyInfoCache.TryGetValue(cacheKey, out propertyInfo)) return true;
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
PropertyInfoCache[cacheKey] = propertyInfo;
return propertyInfo != null;
}
public void Dispose()
{
if (CurrentEditContext == null) return;
CurrentEditContext.OnValidationRequested -= validateModel;
CurrentEditContext.OnFieldChanged -= validateField;
}
}
次に、Blazorの<EditForm>
タグの下に <CustomValidator />
を置く。
<EditForm Model="@model" OnSubmit="@Submit">
<CustomValidator />
<ValidationSummary />
・・・
</EditForm>
それ以外は、普通のバリデーションと同じ要領で、入力用の要素と検証メッセージを表示するタグを置いていく。
<EditForm Model="@model" OnSubmit="@Submit">
<CustomValidator />
<ValidationSummary />
<div>
<label>名前</label>
<input type="text" @bind="@model.Name" />
<ValidationMessage For="@(() => model.Name)" />
</div>
<div>
<label>年齢</label>
<input type="number" @bind="@model.Age" />
<ValidationMessage For="@(() => model.Age)" />
</div>
<div>
<button type="submit">SUBMIT</button>
</div>
</EditForm>
まとめ
ASP .NET Core Blazorでカスタムバリデーションから外部サービスをDI(依存性の注入)する方法を紹介しました。正直、.NETのバグっぽい動きな気がしますが、何とか解決方法が見つかりました。
0 件のコメント:
コメントを投稿