Reputation: 794
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
Upvotes: 5
Views: 1741
Reputation: 780
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);
}
Upvotes: 1
Reputation: 36645
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:
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);
}
}
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;
}
}
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();
}
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 });
}
}
Model design:
public class Booking
{
public int Id { get; set; }
[LandlordNameIsUniqueValidator]
public string UserName { get; set; }
}
Upvotes: 4