将数据库与自定义验证器一起使用
Using a database with Custom Validators
我希望能够创建一个自定义验证器,它将允许我连接到我的数据库并告诉我(例如)名称是否唯一。我曾经在 EF 中使用 [Remote] 属性,但我了解到您不能将其与 Blazor 一起使用。
目前我的验证码是这样的:
public class LandlordNameIsUniqueValidator : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//This is always null
var context = (ApplicationDbContext)validationContext.GetService(typeof(ApplicationDbContext));
var checkName = new LandlordData(context);
var name = value.ToString();
var nameExists = checkName.CheckNameIsUnique(name);
if (!exists)
{
return null;
}
return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
}
}
我使用的代码(在应用程序的其他部分成功)如下,这将return一个布尔值:
public class LandlordData : ILandlordData
{
private readonly ApplicationDbContext _context;
public LandlordData(ApplicationDbContext context)
{
_context = context;
}
public bool CheckNameIsUnique(string name)
{
var exists = _context.Landlords
.AsNoTracking()
.Any(x => x.LandlordName == name);
return exists;
}
}
在StartUp.cs
如下:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
_config.GetConnectionString("DefaultConnection")),
ServiceLifetime.Transient);
我也成功注册了此服务,我在我的 Blazor 页面中使用了它。
services.AddTransient<ILandlordData, LandlordData>();
尽管进行了多次尝试和不同的方法,但我无法(更有可能我不知道如何)注入 DbContext
,因此我可以使用 LandlordData Class
来检查记录。
但是我的ApplicationDbContext
总是空的!
任何人都可以建议访问我的数据库以执行自定义验证的正确方法。
TIA
But my ApplicationDbContext is always null!
可以参考官方文档here。本恩曾说过 ValidationContext.GetService
为空。不支持在 IsValid
方法中注入验证服务。
对于您的场景,您需要先阅读 以了解如何将 IServiceProvider
传递给 ValidationContext
。
详细演示:
自定义DataAnnotationsValidator
public class DIDataAnnotationsValidator: DataAnnotationsValidator
{
[CascadingParameter] EditContext DICurrentEditContext { get; set; }
[Inject]
protected IServiceProvider ServiceProvider { get; set; }
protected override void OnInitialized()
{
if (DICurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
DICurrentEditContext.AddDataAnnotationsValidationWithDI(ServiceProvider);
}
}
自定义EditContextDataAnnotationsExtensions
public static class EditContextDataAnnotationsExtensions
{
private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> _propertyInfoCache
= new ConcurrentDictionary<(Type, string), PropertyInfo>();
public static EditContext AddDataAnnotationsValidationWithDI(this EditContext editContext, IServiceProvider serviceProvider)
{
if (editContext == null)
{
throw new ArgumentNullException(nameof(editContext));
}
var messages = new ValidationMessageStore(editContext);
// Perform object-level validation on request
editContext.OnValidationRequested +=
(sender, eventArgs) => ValidateModel((EditContext)sender, serviceProvider, messages);
// Perform per-field validation on each field edit
editContext.OnFieldChanged +=
(sender, eventArgs) => ValidateField(editContext, serviceProvider, messages, eventArgs.FieldIdentifier);
return editContext;
}
private static void ValidateModel(EditContext editContext, IServiceProvider serviceProvider,ValidationMessageStore messages)
{
var validationContext = new ValidationContext(editContext.Model, serviceProvider, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
messages.Clear();
foreach (var validationResult in validationResults)
{
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
private static void ValidateField(EditContext editContext, IServiceProvider serviceProvider, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
{
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model, serviceProvider, null)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage));
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
editContext.NotifyValidationStateChanged();
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, out PropertyInfo propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
}
将DataAnnotationsValidator
替换为DIDataAnnotationsValidator
<EditForm Model="@book" >
<DIDataAnnotationsValidator /> //change here
<ValidationSummary />
<div class="row content">
<div class="col-md-2"><label for="Name">Name</label></div>
<div class="col-md-3"><InputText id="name" @bind-Value="book.UserName" /></div>
<ValidationMessage For=" (() => book.UserName)" />
</div>
<div class="row content">
<button type="submit">Submit</button>
</div>
</EditForm>
@code {
Booking book= new Booking();
}
然后您可以使用您自定义的验证属性:
public class LandlordNameIsUniqueValidator : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//This is always null
var context = (LykosqlContext)validationContext.GetService(typeof(LykosqlContext));
var checkName = new LandlordData(context);
var name = value.ToString();
var nameExists = checkName.CheckNameIsUnique(name);
return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
}
}
模型设计:
public class Booking
{
public int Id { get; set; }
[LandlordNameIsUniqueValidator]
public string UserName { get; set; }
}
我希望能够创建一个自定义验证器,它将允许我连接到我的数据库并告诉我(例如)名称是否唯一。我曾经在 EF 中使用 [Remote] 属性,但我了解到您不能将其与 Blazor 一起使用。
目前我的验证码是这样的:
public class LandlordNameIsUniqueValidator : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//This is always null
var context = (ApplicationDbContext)validationContext.GetService(typeof(ApplicationDbContext));
var checkName = new LandlordData(context);
var name = value.ToString();
var nameExists = checkName.CheckNameIsUnique(name);
if (!exists)
{
return null;
}
return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
}
}
我使用的代码(在应用程序的其他部分成功)如下,这将return一个布尔值:
public class LandlordData : ILandlordData
{
private readonly ApplicationDbContext _context;
public LandlordData(ApplicationDbContext context)
{
_context = context;
}
public bool CheckNameIsUnique(string name)
{
var exists = _context.Landlords
.AsNoTracking()
.Any(x => x.LandlordName == name);
return exists;
}
}
在StartUp.cs
如下:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
_config.GetConnectionString("DefaultConnection")),
ServiceLifetime.Transient);
我也成功注册了此服务,我在我的 Blazor 页面中使用了它。
services.AddTransient<ILandlordData, LandlordData>();
尽管进行了多次尝试和不同的方法,但我无法(更有可能我不知道如何)注入 DbContext
,因此我可以使用 LandlordData Class
来检查记录。
但是我的ApplicationDbContext
总是空的!
任何人都可以建议访问我的数据库以执行自定义验证的正确方法。
TIA
But my ApplicationDbContext is always null!
可以参考官方文档here。本恩曾说过 ValidationContext.GetService
为空。不支持在 IsValid
方法中注入验证服务。
对于您的场景,您需要先阅读 IServiceProvider
传递给 ValidationContext
。
详细演示:
自定义
DataAnnotationsValidator
public class DIDataAnnotationsValidator: DataAnnotationsValidator { [CascadingParameter] EditContext DICurrentEditContext { get; set; } [Inject] protected IServiceProvider ServiceProvider { get; set; } protected override void OnInitialized() { if (DICurrentEditContext == null) { throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " + $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " + $"inside an EditForm."); } DICurrentEditContext.AddDataAnnotationsValidationWithDI(ServiceProvider); } }
自定义
EditContextDataAnnotationsExtensions
public static class EditContextDataAnnotationsExtensions { private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> _propertyInfoCache = new ConcurrentDictionary<(Type, string), PropertyInfo>(); public static EditContext AddDataAnnotationsValidationWithDI(this EditContext editContext, IServiceProvider serviceProvider) { if (editContext == null) { throw new ArgumentNullException(nameof(editContext)); } var messages = new ValidationMessageStore(editContext); // Perform object-level validation on request editContext.OnValidationRequested += (sender, eventArgs) => ValidateModel((EditContext)sender, serviceProvider, messages); // Perform per-field validation on each field edit editContext.OnFieldChanged += (sender, eventArgs) => ValidateField(editContext, serviceProvider, messages, eventArgs.FieldIdentifier); return editContext; } private static void ValidateModel(EditContext editContext, IServiceProvider serviceProvider,ValidationMessageStore messages) { var validationContext = new ValidationContext(editContext.Model, serviceProvider, null); var validationResults = new List<ValidationResult>(); Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true); // Transfer results to the ValidationMessageStore messages.Clear(); foreach (var validationResult in validationResults) { foreach (var memberName in validationResult.MemberNames) { messages.Add(editContext.Field(memberName), validationResult.ErrorMessage); } } editContext.NotifyValidationStateChanged(); } private static void ValidateField(EditContext editContext, IServiceProvider serviceProvider, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier) { if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo)) { var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model); var validationContext = new ValidationContext(fieldIdentifier.Model, serviceProvider, null) { MemberName = propertyInfo.Name }; var results = new List<ValidationResult>(); Validator.TryValidateProperty(propertyValue, validationContext, results); messages.Clear(fieldIdentifier); messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage)); // We have to notify even if there were no messages before and are still no messages now, // because the "state" that changed might be the completion of some async validation task editContext.NotifyValidationStateChanged(); } } private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, out PropertyInfo propertyInfo) { var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName); if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo)) { // DataAnnotations only validates public properties, so that's all we'll look for // If we can't find it, cache 'null' so we don't have to try again next time propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName); // No need to lock, because it doesn't matter if we write the same value twice _propertyInfoCache[cacheKey] = propertyInfo; } return propertyInfo != null; } }
将
DataAnnotationsValidator
替换为DIDataAnnotationsValidator
<EditForm Model="@book" > <DIDataAnnotationsValidator /> //change here <ValidationSummary /> <div class="row content"> <div class="col-md-2"><label for="Name">Name</label></div> <div class="col-md-3"><InputText id="name" @bind-Value="book.UserName" /></div> <ValidationMessage For=" (() => book.UserName)" /> </div> <div class="row content"> <button type="submit">Submit</button> </div> </EditForm> @code { Booking book= new Booking(); }
然后您可以使用您自定义的验证属性:
public class LandlordNameIsUniqueValidator : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { //This is always null var context = (LykosqlContext)validationContext.GetService(typeof(LykosqlContext)); var checkName = new LandlordData(context); var name = value.ToString(); var nameExists = checkName.CheckNameIsUnique(name); return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName }); } }
模型设计:
public class Booking { public int Id { get; set; } [LandlordNameIsUniqueValidator] public string UserName { get; set; } }