diff --git a/examples/OptionsPatternMvc.Example/Settings/ExampleAppSettings.cs b/examples/OptionsPatternMvc.Example/Settings/ExampleAppSettings.cs index 749df82..1dfab6e 100644 --- a/examples/OptionsPatternMvc.Example/Settings/ExampleAppSettings.cs +++ b/examples/OptionsPatternMvc.Example/Settings/ExampleAppSettings.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using OptionsPatternMvc.Example.Attributes; namespace OptionsPatternMvc.Example.Settings { + [SettingsSectionName("Example")] public class ExampleAppSettings { [Required] diff --git a/examples/OptionsPatternMvc.Example/Settings/Validators/RecursiveDataAnnotationValidateOptions.cs b/examples/OptionsPatternMvc.Example/Settings/Validators/RecursiveDataAnnotationValidateOptions.cs index 61d106b..0151bf7 100644 --- a/examples/OptionsPatternMvc.Example/Settings/Validators/RecursiveDataAnnotationValidateOptions.cs +++ b/examples/OptionsPatternMvc.Example/Settings/Validators/RecursiveDataAnnotationValidateOptions.cs @@ -10,7 +10,7 @@ public class RecursiveDataAnnotationValidateOptions : IValidateOptions where TOptions : class { - private static readonly RecursiveDataAnnotationValidator _recursiveDataAnnotationValidator = new RecursiveDataAnnotationValidator(); + private readonly RecursiveDataAnnotationValidator _recursiveDataAnnotationValidator = new RecursiveDataAnnotationValidator(); public RecursiveDataAnnotationValidateOptions(string optionsBuilderName) { diff --git a/examples/OptionsPatternMvc.Example/appsettings.json b/examples/OptionsPatternMvc.Example/appsettings.json index d9d9a9b..40069c2 100644 --- a/examples/OptionsPatternMvc.Example/appsettings.json +++ b/examples/OptionsPatternMvc.Example/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Example": { + "Name": "OptionsPatternMvc.Example A" + } } diff --git a/src/RecursiveDataAnnotationsValidation/Attributes/SkipRecursiveValidation.cs b/src/RecursiveDataAnnotationsValidation/Attributes/SkipRecursiveValidation.cs index 560e84a..6d50509 100644 --- a/src/RecursiveDataAnnotationsValidation/Attributes/SkipRecursiveValidation.cs +++ b/src/RecursiveDataAnnotationsValidation/Attributes/SkipRecursiveValidation.cs @@ -2,7 +2,7 @@ namespace RecursiveDataAnnotationsValidation.Attributes { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Enum)] + [AttributeUsage(AttributeTargets.Property)] public class SkipRecursiveValidationAttribute : Attribute { diff --git a/src/RecursiveDataAnnotationsValidation/IRecursiveDataAnnotationValidator.cs b/src/RecursiveDataAnnotationsValidation/IRecursiveDataAnnotationValidator.cs index 57ca331..b11e594 100644 --- a/src/RecursiveDataAnnotationsValidation/IRecursiveDataAnnotationValidator.cs +++ b/src/RecursiveDataAnnotationsValidation/IRecursiveDataAnnotationValidator.cs @@ -5,14 +5,14 @@ namespace RecursiveDataAnnotationsValidation { public interface IRecursiveDataAnnotationValidator { - bool TryValidateObjectRecursive( - T obj, + bool TryValidateObjectRecursive( + object obj, ValidationContext validationContext, List validationResults - ) where T : class; + ); - bool TryValidateObjectRecursive( - T obj, + bool TryValidateObjectRecursive( + object obj, List validationResults, IDictionary validationContextItems = null ); diff --git a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs index 5497cfd..cc6fa7b 100644 --- a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs +++ b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs @@ -9,21 +9,21 @@ namespace RecursiveDataAnnotationsValidation { public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator { - public bool TryValidateObjectRecursive( - T obj, + public bool TryValidateObjectRecursive( + object obj, // see Note 1 ValidationContext validationContext, List validationResults - ) where T : class + ) { return TryValidateObjectRecursive( - obj, + obj, validationResults, validationContext.Items ); } - public bool TryValidateObjectRecursive( - T obj, + public bool TryValidateObjectRecursive( + object obj, List validationResults, IDictionary validationContextItems = null ) @@ -40,7 +40,7 @@ private bool TryValidateObject( object obj, ICollection validationResults, IDictionary validationContextItems = null - ) + ) { return Validator.TryValidateObject( obj, @@ -54,8 +54,8 @@ private bool TryValidateObject( ); } - private bool TryValidateObjectRecursive( - T obj, + private bool TryValidateObjectRecursive( + object obj, ICollection validationResults, ISet validatedObjects, IDictionary validationContextItems = null @@ -141,5 +141,24 @@ private bool TryValidateObjectRecursive( return result; } + + /* Note 1: + * + * Background information of why we don't use ValidationContext.ObjectInstance here, even though it is tempting. + * + * https://jeffhandley.com/2009-10-17/validator + * + * It’s important to note that for cross-field validation, relying on the ObjectInstance comes with a caveat. + * It’s possible that the end user has entered a value for a property that could not be set—for instance + * specifying “ABC” for a numeric field. In cases like that, asking the instance for that numeric property + * will of course not give you the “ABC” value that the user has entered, thus the object’s other properties + * are in an indeterminate state. But even so, we’ve found that it’s extremely valuable to provide this object + * instance to the validation attributes. + * + * See also: + * + * https://github.com/dotnet/corefx/blob/8b04d0a18a49448ff7c8ee63239cd6d2a2be7e14/src/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs + * + */ } } \ No newline at end of file diff --git a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationsValidation.csproj b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationsValidation.csproj index 0652360..e5056c9 100644 --- a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationsValidation.csproj +++ b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationsValidation.csproj @@ -6,7 +6,7 @@ Thomas Harold MIT https://github.com/tgharold/RecursiveDataAnnotationsValidation - dataannotation validator + dataannotation validation validator diff --git a/test/RecursiveDataAnnotationsValidation.Tests/RecursiveDataAnnotationValidatorTests.cs b/test/RecursiveDataAnnotationsValidation.Tests/RecursiveDataAnnotationValidatorTests.cs new file mode 100644 index 0000000..1da9b43 --- /dev/null +++ b/test/RecursiveDataAnnotationsValidation.Tests/RecursiveDataAnnotationValidatorTests.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using RecursiveDataAnnotationsValidation.Tests.TestModels; +using Xunit; + +namespace RecursiveDataAnnotationsValidation.Tests +{ + public class RecursiveDataAnnotationValidatorTests + { + /// Tests that use the method which takes a ValidationContext. + public class ValidationContextTests + { + private readonly IRecursiveDataAnnotationValidator _validator = new RecursiveDataAnnotationValidator(); + + [Fact] + public void Pass_all_validation() + { + var sut = new SimpleExample + { + IntegerA = 100, + StringB = "test-100", + BoolC = true, + ExampleEnumD = ExampleEnum.ValueB + }; + + var validationContext = new ValidationContext(sut); + var validationResults = new List(); + var result = _validator.TryValidateObjectRecursive(sut, validationContext, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + [Fact] + public void Indicate_that_IntegerA_is_missing() + { + var sut = new SimpleExample + { + IntegerA = null, + StringB = "test-101", + BoolC = false, + ExampleEnumD = ExampleEnum.ValueC + }; + + const string fieldName = nameof(SimpleExample.IntegerA); + var validationContext = new ValidationContext(sut); + var validationResults = new List(); + var result = _validator.TryValidateObjectRecursive(sut, validationContext, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + Assert.NotNull(validationResults + .FirstOrDefault(x => x.MemberNames.Contains(fieldName))); + } + } + } +} \ No newline at end of file diff --git a/test/RecursiveDataAnnotationsValidation.Tests/SkippedChildrenExampleTests.cs b/test/RecursiveDataAnnotationsValidation.Tests/SkippedChildrenExampleTests.cs index d716604..fa3d23e 100644 --- a/test/RecursiveDataAnnotationsValidation.Tests/SkippedChildrenExampleTests.cs +++ b/test/RecursiveDataAnnotationsValidation.Tests/SkippedChildrenExampleTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using RecursiveDataAnnotationsValidation.Tests.TestModels; using Xunit; @@ -33,5 +34,56 @@ public void Passes_all_validation() Assert.True(result); Assert.Empty(validationResults); } + + [Fact] + public void Fails_for_SimpleA_BoolC() + { + var sut = new SkippedChildrenExample + { + Name = "Skipped-Children-2", + SimpleA = new SimpleExample + { + IntegerA = 75124, + BoolC = null, // set one of the props to null + StringB = "simple-a-child-2", + ExampleEnumD = ExampleEnum.ValueC + }, + SimpleB = new SimpleExample + { + BoolC = true + } + }; + var validationResults = new List(); + var result = _validator.TryValidateObjectRecursive(sut, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + Assert.NotNull(validationResults + .FirstOrDefault(x => x.MemberNames.Contains("SimpleA.BoolC"))); + } + + [Fact] + public void Fails_for_SimpleB_missing() + { + var sut = new SkippedChildrenExample + { + Name = "Skipped-Children-2", + SimpleA = new SimpleExample + { + IntegerA = 75124, + BoolC = null, + StringB = "simple-a-child-2", + ExampleEnumD = ExampleEnum.ValueC + }, + SimpleB = null // the object is missing entirely + }; + var validationResults = new List(); + var result = _validator.TryValidateObjectRecursive(sut, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + Assert.NotNull(validationResults + .FirstOrDefault(x => x.MemberNames.Contains("SimpleB"))); + } } } \ No newline at end of file