diff --git a/src/MiniValidation/TypeDetailsCache.cs b/src/MiniValidation/TypeDetailsCache.cs index aee8fb2..737168d 100644 --- a/src/MiniValidation/TypeDetailsCache.cs +++ b/src/MiniValidation/TypeDetailsCache.cs @@ -212,6 +212,15 @@ private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribut } } + // Deduplicate validation attributes by type and error message + if (validationAttributes is not null) + { + validationAttributes = validationAttributes + .GroupBy(a => a.GetType().FullName + ":" + a.FormatErrorMessage(property.Name)) + .Select(g => g.First()) + .ToList(); + } + return new(validationAttributes?.ToArray(), displayAttribute, skipRecursionAttribute); } diff --git a/tests/MiniValidation.UnitTests/TryValidate.cs b/tests/MiniValidation.UnitTests/TryValidate.cs index 0d59e1e..703a4d0 100644 --- a/tests/MiniValidation.UnitTests/TryValidate.cs +++ b/tests/MiniValidation.UnitTests/TryValidate.cs @@ -464,4 +464,23 @@ public async Task TryValidateAsync_With_Attribute_Attached_Via_TypeDescriptor() Assert.Single(errors["PropertyToBeRequired"]); Assert.Single(errors["AnotherProperty"]); } + + + private class TestResults + { + [MinLength(3)] + public string ErrorCode { get; set; } = string.Empty; + } + + [Fact] + public void MinLengthAttribute_ShouldNotProduceDuplicateErrors() + { + var testObj = new TestResults { ErrorCode = "12" }; + var isValid = MiniValidator.TryValidate(testObj, out var errors); + + Assert.False(isValid); + Assert.True(errors.ContainsKey(nameof(TestResults.ErrorCode))); + var errorMessages = errors[nameof(TestResults.ErrorCode)]; + Assert.Single(errorMessages); + } }