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));
|
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
|
// RequiredAttribute marks a property as required by validation - this means that it
|
||||||
// must have a non-null value on the model during validation.
|
// 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)
|
if (requiredAttribute != null)
|
||||||
{
|
{
|
||||||
context.ValidationMetadata.IsRequired = true;
|
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.
|
// If another provider has already added this attribute, do not repeat it.
|
||||||
// This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and
|
// 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);
|
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
|
// [Required] has no effect on IsBindingRequired
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true)]
|
[InlineData(true)]
|
||||||
|
|
@ -1545,5 +1577,20 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
||||||
|
|
||||||
public string Name { get; private set; }
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
@ -613,6 +614,51 @@ Products: Music Systems, Televisions (3)";
|
||||||
Assert.Empty(content.TextContent);
|
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)
|
private static HttpRequestMessage RequestWithLocale(string url, string locale)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
var response = await client.GetAsync(requestUri);
|
var response = await client.GetAsync(requestUri);
|
||||||
await AssertStatusCodeAsync(response, HttpStatusCode.OK);
|
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 content = await response.Content.ReadAsStringAsync();
|
||||||
var parser = new HtmlParser();
|
var parser = new HtmlParser();
|
||||||
var document = parser.Parse(content);
|
var document = parser.Parse(content);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Testing;
|
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -182,5 +181,67 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Equal("xyz", await response.Content.ReadAsStringAsync());
|
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);
|
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();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IActionResult ValidationProviderAttribute() => View();
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult ValidationProviderAttribute(ValidationProviderAttributeModel model) => View(model);
|
||||||
|
|
||||||
public IActionResult PartialTagHelperWithoutModel() => View();
|
public IActionResult PartialTagHelperWithoutModel() => View();
|
||||||
|
|
||||||
public IActionResult StatusMessage() => View(new StatusMessageModel { Message = "Some status message"});
|
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