10

We're on .NET Core 3.1.5 and this is a Blazor Server application.

We have a ValidationAttribute and need access to an external Service to validate the objects.

ValidationAttribute has the IsValid method:

protected override ValidationResult IsValid(object value, ValidationContext validationContext) ValidationContext has a GetService method which delegates to an instance of ServiceProvider. Unfortunately, the service provider field is never initialized and so we cannot retrieve any Services.

This was raised (and fixed) back in Mvc: aspnet/Mvc#6346 But our Validator is called via one of these two:

https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L47 https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L75 And later down the stack the service provider is also never set. I hesitated to open a bug (but can do so) but this seems wrong to me (or at least should be documented).

Any Google search eventually ends up at this Blog post but as I just mentioned this doesn't work.

So our question is: What's the correct way of injecting services into a ValidationAttribute or more general what is the proper way to validate a field of a model that requires a call to an external service?

In statup.cs:

services.AddTransient<IMarktTypDaten, MarktTypDaten>(); 

Class where we are trying to inject the service and apply the validation.

public class MarktTypNameValidation : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var service = (IMarktTypDaten) validationContext.GetRequiredService(typeof(IMarktTypDaten)); ...some code... return ValidationResult.Success; } } 

ExceptionMessage when calling GetRequiredService: 'No service for type 'DataAccessLibrary.Interfaces.IMarktTypDaten' has been registered.

It's also posted on Github: https://github.com/dotnet/aspnetcore/discussions/23305

Also: I'm using C#/.NET for the first time in 15 or so years, please be gentle ;-)

5
  • I tried and get the same error. Following your question. I tried on both serverside and wasm ( blazorfiddle.com/s/gk50cc8v ) Commented Jun 24, 2020 at 17:38
  • 2
    "We have a ValidationAttribute and need access to an external Service to validate the objects." You shouldn't. Attributes should be passive, or at most contain logic that doesn't do anything impure (e.g. I/O). Instead, move that logic to a separate validation service. Commented Jun 24, 2020 at 19:28
  • 1
    @Steven, I understand that this is an antipattern and I appreciate your comment about right approach but, the issue is: documentation says we can access to DI services but we can't. Do you know why we can't access to service as expected? Regards. Commented Jun 24, 2020 at 20:16
  • 1
    @daniherrera thank you for trying it out in both server & wasm and thank you Steven for the explanation, that helps. I wish the documentation would have been clearer. But I agree with Dani that this still looks like a bug. Commented Jun 24, 2020 at 22:20
  • @LarsFrancke, I saw your links to MS issues, nice work! Are you so kind to post an answer to question here? Commented Jun 26, 2020 at 19:20

2 Answers 2

9

My team has heavily invested in our custom validation code which underneath uses DataAnnotations for validation. Specifically our custom validators (through much abstraction) depends on the ValidationAttribute.IsValid method and the fact that the ValidationContext parameter passed into it is itself an IServiceProvider. This has worked good for us in MVC.

We're currently integrating server side Blazor into an existing MVC app which has many validators already implemented with our custom validation (all based on DataAnnotations) and we'd like to take advantage of these within our Blazor validation. Although the argument of, "you're not supposed to do that" is probably valid, we are far beyond that option without major refactoring.

So I dug deeper and discovered that we could make a relatively small change to Microsoft's DataAnnotationsValidator.cs type located here. https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/DataAnnotationsValidator.cs

The real change is actually in the EditContextDataAnnotationsExtensions.cs type that's located here: https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

Specifically, the EditContextDataAnnotationsExtensions method actually creates a new ValidationContext object but does NOT initialize the service provider. I've created a CustomValidator component to replace the DataAnnotationsValidator component and copied most of the flow (I changed the code to fit more with our style but the flow of things are the same).

In our CustomValidator I've included an initialization of the ValidationContext's service provider.

 var validationContext = new ValidationContext(editContext.Model); validationContext.InitializeServiceProvider(type => this.serviceProvider.GetService(type)); 

Here's my code, slightly edited but the following should work out of the box.

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); // Perform object-level validation on request CurrentEditContext.OnValidationRequested += validateModel; // Perform per-field validation on each field edit 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); // Transfer results to the ValidationMessageStore 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)); // 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 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; // 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; } public void Dispose() { if (CurrentEditContext == null) return; CurrentEditContext.OnValidationRequested -= validateModel; CurrentEditContext.OnFieldChanged -= validateField; } } 

All that is needed after you add this type is to use it instead of the DataAnnotationsValidator within your blazor/razor file.

So instead of this:

<DataAnnotationsValidator /> 

do this:

<CustomValidator /> 
Sign up to request clarification or add additional context in comments.

3 Comments

This is a great solution for those who want to keep the validation logic in IValidatableObject.Validate(). It is also possible to do similar chages to make ValidateComplexTypeAttribute works: create/copy CustomValidateComplexTypeAttribute and CustomObjectGraphDataAnnotationsValidator classes, add InitializeServiceProvider and refereces to the custom classes.
I have created an issue on GitHub for this problem.
@TheBigNeo It will be interesting to see how your GitHub issue is resolved. I almost think there's a built in solution now. Maybe with .net 8 but I haven't had time to verify that.
5

As suggested by Steven in the comments section, you shouldn't do that that way. Instead you can do that as described in the following code snippet, part of which is only pseudocode to point out what you need to do... It is not suppose to work as is.

You can overrides the EditContext's FieldChanged method for this.

Suppose you have this form with an input field for the email address, and you want to check if this email is already being used by another user... To check the availability of the entered email address you must perform a call to your data store and verify this. Note that some of the actions described in the FieldChanged method can be moved to a separate validation service...

<EditForm EditContext="@EditContext" OnValidSubmit="HandleValidSubmit"> <DataAnnotationsValidator /> <div class="form-group"> <label for="name">Name: </label> <InputText Id="name" Class="form-control" @bind- Value="@Model.Name"></InputText> <ValidationMessage For="@(() => Model.Name)" /> </div> <div class="form-group"> <label for="body">Text: </label> <InputText Id="body" Class="form-control" @bind-Value="@Model.Text"></InputText> <ValidationMessage For="@(() => Model.Text)" /> </div> <div class="form-group"> <label for="body">Email: </label> <InputText Id="body" Class="form-control" @bind-Value="@Model.EmailAddress"></InputText> <ValidationMessage For="@(() => Model.EmailAddress)" /> </div> <p> <button type="submit">Save</button> </p> </EditForm> @code { private EditContext EditContext; private Comment Model = new Comment(); ValidationMessageStore messages; protected override void OnInitialized() { EditContext = new EditContext(Model); EditContext.OnFieldChanged += EditContext_OnFieldChanged; messages = new ValidationMessageStore(EditContext); base.OnInitialized(); } // Note: The OnFieldChanged event is raised for each field in the // model. Here you should validate the email address private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e) { // Call your database to check if the email address is // available // Retrieve the value of the input field for email // Pseudocode... var email = "[email protected]"; var exists = VerifyEmail(email); messages.Clear(); // If exists is true, form a message about this, and add it // to the messages object so that it is displayed in the // ValidationMessage component for email } } 

Hope this helps...

6 Comments

Thank you for the answer. Very helpful! I now found some documentation here learn.microsoft.com/en-us/aspnet/core/blazor/… but it doesn't explicitly spell out what you said here. That'd be a useful addition.
@LarsFrancke I'd like to encourage you to give feedback the .Net guzs about the documentation. They are going to extend it/fix it. Thank you! :)
Thanks @SayusiAndo. I raised an issue about the "original" problem here github.com/dotnet/aspnetcore/issues/23380
Lars Francke, DI is not supported in DataAnnotations. You can instead use FluentValidation. It's really easy. Here's a link to a good sample by Chris Sainty: chrissainty.com/…, then you can see DI in the current context: github.com/Blazored/FluentValidation . Note that the suggestion in my answer is still remained relevant in any case, the flow must be similar, and the use of EditContext.OnFieldChanged is a must, no matter what Validator you're using (DataAnnotations or FluentValidation).
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.