5

I want to be able to create a custom validator, that will allow me to connect to my database and tell me (for example) whether a name is unique. I used to use the [Remote] attribute in EF, but I have read that you cannot use this with Blazor.

The Validation code I have so far is this:

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 });
        }
    }

The code I use (successfully in other parts of the application) is as follows, this will return a bool:

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;
   }
}

In StartUp.cs is as follows:

 services.AddDbContext<ApplicationDbContext>(options =>
               options.UseSqlServer(
                   _config.GetConnectionString("DefaultConnection")),
                   ServiceLifetime.Transient);

I also have this service registered, which I use in my Blazor pages, successfully.

 services.AddTransient<ILandlordData, LandlordData>();

Despite numerous attempts and different methods, I cannot (more likely I don't know how to) inject the DbContext, so I can use the LandlordData Class to check the record.

But my ApplicationDbContext is always null!

Can anyone advise the correct approach to access my database to perform custom validation.

TIA

2
  • Hi @Rena, I got pulled onto something else, I am going to give it a go tonight, so I will have a reply for you. Thanks for your help. Commented Apr 16, 2021 at 12:56
  • OrangeGoblin, I have my DTOs for the EditForm separate from my database (which depends on the DTO project), how would I go about validating them? Commented May 8, 2024 at 13:11

2 Answers 2

4

But my ApplicationDbContext is always null!

You could refer to the official document here. It has benn said that ValidationContext.GetService is null. Injecting services for validation in the IsValid method isn't supported.

For your scenario, you need firstly read the answer to learn how to pass IServiceProvider to ValidationContext.

Detailed demo:

  1. Custom 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. Custom 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. Replace DataAnnotationsValidator with 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. Then you could use your customed validation attribute:

    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. Model design:

    public class Booking
    {
        public int Id { get; set; }
        [LandlordNameIsUniqueValidator]
        public string UserName { get; set; }
    }
    
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for your reply. But for me, this is far too complex to fire a validation message when something is "true". I think I need to change the question. I have since discovered you cannot use the connectivity I have in EF and use Custom Validators, I have also discovered, you should never use Fluent Validation for server side results. So I am amazed this has become so complicated.
You may misunderstand the code here. The code is complex that is because the default ValidationAttribute does not support getting the service. So you need to rewrite the source code to make get service work. Then your requirement will work fine in LandlordNameIsUniqueValidator .
I don't understand how it works, but it works really well. I need to put some more study time into validators. Thank you for your support and apologies for the long reply.
I was able to accomplish this with just steps 4 & 5.
1

GetService returns null when the validation context service provider does not have the service (DbContext) registered.

Here's a custom validator that uses a stringHelper service which is used in the validator.

Calling the validator

using Microsoft.Extensions.DependencyInjection;

...

    var serviceProvider = new ServiceCollection()
    .AddSingleton<IStringHelper, StringHelper>()
    .BuildServiceProvider();

    var context = new ValidationContext(yourObjectRequiringValidation,serviceProvider,null);
    var results = new List<ValidationResult>();
    var isValid = Validator.TryValidateObject(yourObjectRequiringValidation, context, results, true);

And the custom validator that uses string helper service:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
       var stringValue = value?.ToString();
        
       var stringHelper = (IStringHelper)validationContext.GetService(typeof(IStringHelper));
        
       if (stringHelper == null)
          throw new InvalidOperationException("The string helper service has not been registered in the validation context service provider and so GetService cannot find the service string helper. ");
        
       return stringHelper.IsValidString(stringValue) ? ValidationResult.Success : new ValidationResult(this.ErrorMessageString);
    
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.