Skip to content

Commit

Permalink
Added IServiceProvider to DataAnnotationsValidator (dotnet#39445)
Browse files Browse the repository at this point in the history
  • Loading branch information
MariovanZeist authored and ShreyasJejurkar committed Jan 22, 2022
1 parent 9b14ab1 commit 68ff4c0
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 13 deletions.
4 changes: 3 additions & 1 deletion src/Components/Forms/src/DataAnnotationsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class DataAnnotationsValidator : ComponentBase, IDisposable

[CascadingParameter] EditContext? CurrentEditContext { get; set; }

[Inject] private IServiceProvider ServiceProvider { get; set; } = default!;

/// <inheritdoc />
protected override void OnInitialized()
{
Expand All @@ -23,7 +25,7 @@ protected override void OnInitialized()
$"inside an EditForm.");
}

_subscriptions = CurrentEditContext.EnableDataAnnotationsValidation();
_subscriptions = CurrentEditContext.EnableDataAnnotationsValidation(ServiceProvider);
_originalEditContext = CurrentEditContext;
}

Expand Down
26 changes: 22 additions & 4 deletions src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,26 @@ public static EditContext AddDataAnnotationsValidation(this EditContext editCont
/// </summary>
/// <param name="editContext">The <see cref="EditContext"/>.</param>
/// <returns>A disposable object whose disposal will remove DataAnnotations validation support from the <see cref="EditContext"/>.</returns>
[Obsolete("This API is obsolete and may be removed in future versions.")]
public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext)
{
return new DataAnnotationsEventSubscriptions(editContext);
return new DataAnnotationsEventSubscriptions(editContext, null!);
}
/// <summary>
/// Enables DataAnnotations validation support for the <see cref="EditContext"/>.
/// </summary>
/// <param name="editContext">The <see cref="EditContext"/>.</param>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used in the <see cref="ValidationContext"/>.</param>
/// <returns>A disposable object whose disposal will remove DataAnnotations validation support from the <see cref="EditContext"/>.</returns>
public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext, IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
return new DataAnnotationsEventSubscriptions(editContext, serviceProvider);
}


private static event Action? OnClearCache;

Expand All @@ -50,11 +66,13 @@ private sealed class DataAnnotationsEventSubscriptions : IDisposable
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();

private readonly EditContext _editContext;
private readonly IServiceProvider? _serviceProvider;
private readonly ValidationMessageStore _messages;

public DataAnnotationsEventSubscriptions(EditContext editContext)
public DataAnnotationsEventSubscriptions(EditContext editContext, IServiceProvider serviceProvider)
{
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
_serviceProvider = serviceProvider;
_messages = new ValidationMessageStore(_editContext);

_editContext.OnFieldChanged += OnFieldChanged;
Expand All @@ -72,7 +90,7 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model)
var validationContext = new ValidationContext(fieldIdentifier.Model, _serviceProvider, items: null)
{
MemberName = propertyInfo.Name
};
Expand All @@ -93,7 +111,7 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)

private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
var validationContext = new ValidationContext(_editContext.Model);
var validationContext = new ValidationContext(_editContext.Model, _serviceProvider, items: null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);

Expand Down
1 change: 1 addition & 0 deletions src/Components/Forms/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
static Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation(this Microsoft.AspNetCore.Components.Forms.EditContext! editContext, System.IServiceProvider! serviceProvider) -> System.IDisposable!
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components.Test.Helpers;

namespace Microsoft.AspNetCore.Components.Forms;

public class EditContextDataAnnotationsExtensionsTest
{
private static readonly IServiceProvider _serviceProvider = new TestServiceProvider();

[Fact]
public void CannotUseNullEditContext()
{
var editContext = (EditContext)null;
var ex = Assert.Throws<ArgumentNullException>(() => editContext.EnableDataAnnotationsValidation());
var ex = Assert.Throws<ArgumentNullException>(() => editContext.EnableDataAnnotationsValidation(_serviceProvider));
Assert.Equal("editContext", ex.ParamName);
}

Expand All @@ -31,7 +34,7 @@ public void GetsValidationMessagesFromDataAnnotations()
// Arrange
var model = new TestModel { IntFrom1To100 = 101 };
var editContext = new EditContext(model);
editContext.EnableDataAnnotationsValidation();
editContext.EnableDataAnnotationsValidation(_serviceProvider);

// Act
var isValid = editContext.Validate();
Expand Down Expand Up @@ -61,7 +64,7 @@ public void ClearsExistingValidationMessagesOnFurtherRuns()
// Arrange
var model = new TestModel { IntFrom1To100 = 101 };
var editContext = new EditContext(model);
editContext.EnableDataAnnotationsValidation();
editContext.EnableDataAnnotationsValidation(_serviceProvider);

// Act/Assert 1: Initially invalid
Assert.False(editContext.Validate());
Expand All @@ -78,7 +81,7 @@ public void NotifiesValidationStateChangedAfterObjectValidation()
// Arrange
var model = new TestModel { IntFrom1To100 = 101 };
var editContext = new EditContext(model);
editContext.EnableDataAnnotationsValidation();
editContext.EnableDataAnnotationsValidation(_serviceProvider);
var onValidationStateChangedCount = 0;
editContext.OnValidationStateChanged += (sender, eventArgs) => onValidationStateChangedCount++;

Expand Down Expand Up @@ -106,7 +109,7 @@ public void PerformsPerPropertyValidationOnFieldChange()
var model = new TestModel { IntFrom1To100 = 101 };
var independentTopLevelModel = new object(); // To show we can validate things on any model, not just the top-level one
var editContext = new EditContext(independentTopLevelModel);
editContext.EnableDataAnnotationsValidation();
editContext.EnableDataAnnotationsValidation(_serviceProvider);
var onValidationStateChangedCount = 0;
var requiredStringIdentifier = new FieldIdentifier(model, nameof(TestModel.RequiredString));
var intFrom1To100Identifier = new FieldIdentifier(model, nameof(TestModel.IntFrom1To100));
Expand Down Expand Up @@ -146,7 +149,7 @@ public void IgnoresFieldChangesThatDoNotCorrespondToAValidatableProperty(string
{
// Arrange
var editContext = new EditContext(new TestModel());
editContext.EnableDataAnnotationsValidation();
editContext.EnableDataAnnotationsValidation(_serviceProvider);
var onValidationStateChangedCount = 0;
editContext.OnValidationStateChanged += (sender, eventArgs) => onValidationStateChangedCount++;

Expand All @@ -165,7 +168,7 @@ public void CanDetachFromEditContext()
// Arrange
var model = new TestModel { IntFrom1To100 = 101 };
var editContext = new EditContext(model);
var subscription = editContext.EnableDataAnnotationsValidation();
var subscription = editContext.EnableDataAnnotationsValidation(_serviceProvider);

// Act/Assert 1: when we're attached
Assert.False(editContext.Validate());
Expand Down
20 changes: 20 additions & 0 deletions src/Components/test/E2ETest/Tests/FormsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ public async Task EditFormWorksWithDataAnnotationsValidator()
Browser.Equal("OnValidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
}

[Fact]
public void EditFormWorksWithDataAnnotationsValidatorAndDI()
{
var appElement = Browser.MountTestComponent<ValidationComponentDI>();
var form = appElement.FindElement(By.TagName("form"));
var userNameInput = appElement.FindElement(By.ClassName("the-quiz")).FindElement(By.TagName("input"));
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);

userNameInput.SendKeys("Bacon\t");
submitButton.Click();
//We can only have this errormessage when DI is working
Browser.Equal(new[] { "You should not put that in a salad!" }, messagesAccessor);

userNameInput.Clear();
userNameInput.SendKeys("Watermelon\t");
submitButton.Click();
Browser.Empty(messagesAccessor);
}

[Fact]
public void InputTextInteractsWithEditContext()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
protected override void OnInitialized()
{
editContext = new EditContext(person);
editContext.EnableDataAnnotationsValidation();
editContext.EnableDataAnnotationsValidation(new TestServiceProvider());

// Wire up INotifyPropertyChanged to the EditContext
person.PropertyChanged += (sender, eventArgs) =>
Expand Down Expand Up @@ -103,4 +103,10 @@

#endregion
}

public class TestServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
=> throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms

<EditForm Model="@this" autocomplete="off">
<DataAnnotationsValidator />

<p class="the-quiz">
Name something you can put in a salad:
<input @bind="SaladIngredient" class="@context.FieldCssClass(() => SaladIngredient)" />
</p>

<button type="submit">Submit</button>
<ul class="validation-errors">
@foreach (var message in context.GetValidationMessages())
{
<li class="validation-message">@message</li>
}
</ul>

</EditForm>

@code {
[SaladChefValidator]
public string SaladIngredient { get; set; }

public class SaladChefValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var saladChef = validationContext.GetRequiredService<SaladChef>();
if (saladChef.ThingsYouCanPutInASalad.Contains(value.ToString()))
{
return ValidationResult.Success;
}
return new ValidationResult("You should not put that in a salad!");
}
}

// Simple class to check if DI can be used in Validation attributes
public class SaladChef
{
public string[] ThingsYouCanPutInASalad = { "Strawberries", "Pineapple", "Honeydew", "Watermelon", "Grapes" };
}
}
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<option value="BasicTestApp.FormsTest.SimpleValidationComponentUsingExperimentalValidator">Simple validation using experimental validator</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponentUsingExperimentalValidator">Typical validation using experimental validator</option>
<option value="BasicTestApp.FormsTest.ValidationComponentDI">Validation with Dependency Injection</option>
<option value="BasicTestApp.FormsTest.InputFileComponent">Input file</option>
<option value="BasicTestApp.FormsTest.InputRangeComponent">Input range</option>
<option value="BasicTestApp.FormsTest.InputsWithoutEditForm">Inputs without EditForm</option>
Expand Down
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static async Task Main(string[] args)
});

builder.Services.AddScoped<PreserveStateService>();
builder.Services.AddTransient<FormsTest.ValidationComponentDI.SaladChef>();

builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));

Expand Down
1 change: 1 addition & 0 deletions src/Components/test/testassets/TestServer/ServerStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public void ConfigureServices(IServiceCollection services)
javaScriptInitializer: "myJsRootComponentInitializers.testInitializer");
});
services.AddSingleton<ResourceRequestLog>();
services.AddTransient<BasicTestApp.FormsTest.ValidationComponentDI.SaladChef>();

// Since tests run in parallel, we use an ephemeral key provider to avoid filesystem
// contention issues.
Expand Down

0 comments on commit 68ff4c0

Please sign in to comment.