CompositeValidationAttribute
- Add abstract CompositeValidationAttribute. - Change DataAnnotationsMetadataProvider.CreateValidationMetadata to populate ValidatorMetadata with validation attributes from CompositeValidationAttribute.
This commit is contained in:
parent
5c488bf09c
commit
d4beab5d09
|
|
@ -317,15 +317,30 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var attributes = new List<object>(context.Attributes.Count);
|
||||
|
||||
for (var i = 0; i < context.Attributes.Count; i++)
|
||||
{
|
||||
var attribute = context.Attributes[i];
|
||||
if (attribute is ValidationProviderAttribute validationProviderAttribute)
|
||||
{
|
||||
attributes.AddRange(validationProviderAttribute.GetValidationAttributes());
|
||||
}
|
||||
else
|
||||
{
|
||||
attributes.Add(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredAttribute marks a property as required by validation - this means that it
|
||||
// must have a non-null value on the model during validation.
|
||||
var requiredAttribute = context.Attributes.OfType<RequiredAttribute>().FirstOrDefault();
|
||||
var requiredAttribute = attributes.OfType<RequiredAttribute>().FirstOrDefault();
|
||||
if (requiredAttribute != null)
|
||||
{
|
||||
context.ValidationMetadata.IsRequired = true;
|
||||
}
|
||||
|
||||
foreach (var attribute in context.Attributes.OfType<ValidationAttribute>())
|
||||
foreach (var attribute in attributes.OfType<ValidationAttribute>())
|
||||
{
|
||||
// If another provider has already added this attribute, do not repeat it.
|
||||
// This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract class for grouping attributes of type <see cref="ValidationAttribute"/> into
|
||||
/// one <see cref="Attribute"/>
|
||||
/// </summary>
|
||||
public abstract class ValidationProviderAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets <see cref="ValidationAttribute" /> instances associated with this attribute.
|
||||
/// </summary>
|
||||
/// <returns>Sequence of <see cref="ValidationAttribute" /> associated with this attribute.</returns>
|
||||
public abstract IEnumerable<ValidationAttribute> GetValidationAttributes();
|
||||
}
|
||||
}
|
||||
|
|
@ -1241,6 +1241,38 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
Assert.Equal(initialValue, context.ValidationMetadata.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationMetadata_WillAddValidationAttributes_From_ValidationProviderAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var validationProviderAttribute = new FooCompositeValidationAttribute(
|
||||
attributes: new List<ValidationAttribute>
|
||||
{
|
||||
new RequiredAttribute(),
|
||||
new StringLengthAttribute(5)
|
||||
});
|
||||
|
||||
var attributes = new Attribute[] { new EmailAddressAttribute(), validationProviderAttribute };
|
||||
var key = ModelMetadataIdentity.ForProperty(typeof(string), "Length", typeof(string));
|
||||
var context = new ValidationMetadataProviderContext(key, GetModelAttributes(new object[0], attributes));
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
var expected = new List<object>
|
||||
{
|
||||
new EmailAddressAttribute(),
|
||||
new RequiredAttribute(),
|
||||
new StringLengthAttribute(5)
|
||||
};
|
||||
|
||||
Assert.Equal(expected, actual: context.ValidationMetadata.ValidatorMetadata);
|
||||
}
|
||||
|
||||
// [Required] has no effect on IsBindingRequired
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
|
|
@ -1545,5 +1577,20 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
|
||||
public string Name { get; private set; }
|
||||
}
|
||||
|
||||
private class FooCompositeValidationAttribute : ValidationProviderAttribute
|
||||
{
|
||||
private IEnumerable<ValidationAttribute> _attributes;
|
||||
|
||||
public FooCompositeValidationAttribute(IEnumerable<ValidationAttribute> attributes)
|
||||
{
|
||||
_attributes = attributes;
|
||||
}
|
||||
|
||||
public override IEnumerable<ValidationAttribute> GetValidationAttributes()
|
||||
{
|
||||
return _attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
|
@ -613,6 +614,51 @@ Products: Music Systems, Televisions (3)";
|
|||
Assert.Empty(content.TextContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProviderAttribute_ValidationTagHelpers_GeneratesExpectedDataAttributes()
|
||||
{
|
||||
// Act
|
||||
var document = await Client.GetHtmlDocumentAsync("HtmlGeneration_Home/ValidationProviderAttribute");
|
||||
|
||||
// Assert
|
||||
var firstName = document.RequiredQuerySelector("#FirstName");
|
||||
Assert.Equal("true", firstName.GetAttribute("data-val"));
|
||||
Assert.Equal("The FirstName field is required.", firstName.GetAttribute("data-val-required"));
|
||||
Assert.Equal("The field FirstName must be a string with a maximum length of 5.", firstName.GetAttribute("data-val-length"));
|
||||
Assert.Equal("5", firstName.GetAttribute("data-val-length-max"));
|
||||
Assert.Equal("The field FirstName must match the regular expression '[A-Za-z]*'.", firstName.GetAttribute("data-val-regex"));
|
||||
Assert.Equal("[A-Za-z]*", firstName.GetAttribute("data-val-regex-pattern"));
|
||||
|
||||
var lastName = document.RequiredQuerySelector("#LastName");
|
||||
Assert.Equal("true", lastName.GetAttribute("data-val"));
|
||||
Assert.Equal("The LastName field is required.", lastName.GetAttribute("data-val-required"));
|
||||
Assert.Equal("The field LastName must be a string with a maximum length of 6.", lastName.GetAttribute("data-val-length"));
|
||||
Assert.Equal("6", lastName.GetAttribute("data-val-length-max"));
|
||||
Assert.False(lastName.HasAttribute("data-val-regex"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProviderAttribute_ValidationTagHelpers_GeneratesExpectedSpansAndDivsOnValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "HtmlGeneration_Home/ValidationProviderAttribute");
|
||||
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "FirstName", "TestFirstName" },
|
||||
});
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var document = await response.GetHtmlDocumentAsync();
|
||||
Assert.Collection(
|
||||
document.QuerySelectorAll("div.validation-summary-errors ul li"),
|
||||
item => Assert.Equal("The field FirstName must be a string with a maximum length of 5.", item.TextContent),
|
||||
item => Assert.Equal("The LastName field is required.", item.TextContent));
|
||||
}
|
||||
|
||||
private static HttpRequestMessage RequestWithLocale(string url, string locale)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
var response = await client.GetAsync(requestUri);
|
||||
await AssertStatusCodeAsync(response, HttpStatusCode.OK);
|
||||
|
||||
return await GetHtmlDocumentAsync(response);
|
||||
}
|
||||
|
||||
public static async Task<IHtmlDocument> GetHtmlDocumentAsync(this HttpResponseMessage response)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var parser = new HtmlParser();
|
||||
var document = parser.Parse(content);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
|
@ -182,5 +181,67 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("xyz", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProviderAttribute_WillValidateObject()
|
||||
{
|
||||
// Arrange
|
||||
var invalidRequestData = "{\"FirstName\":\"TestName123\", \"LastName\": \"Test\"}";
|
||||
var content = new StringContent(invalidRequestData, Encoding.UTF8, "application/json");
|
||||
var expectedErrorMessage =
|
||||
"{\"FirstName\":[\"The field FirstName must match the regular expression '[A-Za-z]*'.\"," +
|
||||
"\"The field FirstName must be a string with a maximum length of 5.\"]}";
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync(
|
||||
"http://localhost/Validation/ValidationProviderAttribute", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected: StatusCodes.Status400BadRequest, actual: (int)response.StatusCode);
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedErrorMessage, actual: responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProviderAttribute_DoesNotInterfere_WithOtherValidationAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var invalidRequestData = "{\"FirstName\":\"Test\", \"LastName\": \"Testsson\"}";
|
||||
var content = new StringContent(invalidRequestData, Encoding.UTF8, "application/json");
|
||||
var expectedErrorMessage =
|
||||
"{\"LastName\":[\"The field LastName must be a string with a maximum length of 5.\"]}";
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync(
|
||||
"http://localhost/Validation/ValidationProviderAttribute", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected: StatusCodes.Status400BadRequest, actual: (int)response.StatusCode);
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedErrorMessage, actual: responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProviderAttribute_RequiredAttributeErrorMessage_WillComeFirst()
|
||||
{
|
||||
// Arrange
|
||||
var invalidRequestData = "{\"FirstName\":\"Testname\", \"LastName\": \"\"}";
|
||||
var content = new StringContent(invalidRequestData, Encoding.UTF8, "application/json");
|
||||
var expectedError =
|
||||
"{\"LastName\":[\"The LastName field is required.\"]," +
|
||||
"\"FirstName\":[\"The field FirstName must be a string with a maximum length of 5.\"]}";
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync(
|
||||
"http://localhost/Validation/ValidationProviderAttribute", content);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected: StatusCodes.Status400BadRequest, actual: (int)response.StatusCode);
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal(expectedError, actual: responseContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,5 +67,16 @@ namespace FormatterWebSite
|
|||
{
|
||||
return Json(simpleTypePropertiesModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult ValidationProviderAttribute([FromBody] ValidationProviderAttributeModel validationProviderAttributeModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Mvc.DataAnnotations;
|
||||
|
||||
namespace FormatterWebSite
|
||||
{
|
||||
public class ValidationProviderAttributeModel
|
||||
{
|
||||
[FirstName]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[StringLength(maximumLength: 5)]
|
||||
[LastName]
|
||||
public string LastName { get; set; }
|
||||
}
|
||||
|
||||
public class FirstNameAttribute : ValidationProviderAttribute
|
||||
{
|
||||
public override IEnumerable<ValidationAttribute> GetValidationAttributes()
|
||||
{
|
||||
return new List<ValidationAttribute>
|
||||
{
|
||||
new RequiredAttribute(),
|
||||
new RegularExpressionAttribute(pattern: "[A-Za-z]*"),
|
||||
new StringLengthAttribute(maximumLength: 5)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class LastNameAttribute : ValidationProviderAttribute
|
||||
{
|
||||
public override IEnumerable<ValidationAttribute> GetValidationAttributes()
|
||||
{
|
||||
return new List<ValidationAttribute>
|
||||
{
|
||||
new RequiredAttribute()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,6 +238,11 @@ namespace HtmlGenerationWebSite.Controllers
|
|||
return View();
|
||||
}
|
||||
|
||||
public IActionResult ValidationProviderAttribute() => View();
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult ValidationProviderAttribute(ValidationProviderAttributeModel model) => View(model);
|
||||
|
||||
public IActionResult PartialTagHelperWithoutModel() => View();
|
||||
|
||||
public IActionResult StatusMessage() => View(new StatusMessageModel { Message = "Some status message"});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Mvc.DataAnnotations;
|
||||
|
||||
namespace HtmlGenerationWebSite.Models
|
||||
{
|
||||
public class ValidationProviderAttributeModel
|
||||
{
|
||||
[FirstName]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[StringLength(maximumLength: 6)]
|
||||
[LastName]
|
||||
public string LastName { get; set; }
|
||||
}
|
||||
|
||||
public class FirstNameAttribute : ValidationProviderAttribute
|
||||
{
|
||||
public override IEnumerable<ValidationAttribute> GetValidationAttributes()
|
||||
{
|
||||
return new List<ValidationAttribute>
|
||||
{
|
||||
new RequiredAttribute(),
|
||||
new RegularExpressionAttribute(pattern: "[A-Za-z]*"),
|
||||
new StringLengthAttribute(maximumLength: 5)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class LastNameAttribute : ValidationProviderAttribute
|
||||
{
|
||||
public override IEnumerable<ValidationAttribute> GetValidationAttributes()
|
||||
{
|
||||
return new List<ValidationAttribute>
|
||||
{
|
||||
new RequiredAttribute()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@model HtmlGenerationWebSite.Models.ValidationProviderAttributeModel
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<form asp-controller="HtmlGeneration_ValidationProviderAttribute" asp-action="Index">
|
||||
<div>
|
||||
<label asp-for="FirstName"></label>
|
||||
<input asp-for="FirstName" type="text" class="form-control" />
|
||||
<span asp-validation-for="FirstName"></span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label asp-for="LastName"></label>
|
||||
<input asp-for="LastName" type="text" class="form-control" />
|
||||
<span asp-validation-for="LastName"></span>
|
||||
</div>
|
||||
|
||||
<div asp-validation-summary="All"></div>
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue