将数据库与自定义验证器一起使用

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

详细演示:

  1. 自定义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);
        }
    }
    
  2. 自定义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;
        }
    
    }
    
  3. 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();
    }
    
  4. 然后您可以使用您自定义的验证属性:

    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 });
        }
    }
    
  5. 模型设计:

    public class Booking
    {
        public int Id { get; set; }
        [LandlordNameIsUniqueValidator]
        public string UserName { get; set; }
    }