Merge remote-tracking branch 'origin/release/2.2'

This commit is contained in:
Pranav K 2018-07-20 17:09:38 -07:00
commit 2081abd75d
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
13 changed files with 364 additions and 4 deletions

View File

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2036
MinimumVisualStudioVersion = 15.0.26730.03
@ -114,6 +114,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mvc.Analyzers.Test", "test\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Views.TestCommon", "test\Microsoft.AspNetCore.Mvc.Views.TestCommon\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj", "{0772E545-A674-4165-9469-E3D79D88A4A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Mvc.Testing", "src\Microsoft.AspNetCore.Mvc.Testing\Microsoft.AspNetCore.Mvc.Testing.csproj", "{92D959F2-66B8-490A-BA33-DA4421EBC948}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -560,6 +562,18 @@ Global
{0772E545-A674-4165-9469-E3D79D88A4A8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{0772E545-A674-4165-9469-E3D79D88A4A8}.Release|x86.ActiveCfg = Release|Any CPU
{0772E545-A674-4165-9469-E3D79D88A4A8}.Release|x86.Build.0 = Release|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Debug|x86.ActiveCfg = Debug|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Debug|x86.Build.0 = Debug|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Release|Any CPU.Build.0 = Release|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Release|x86.ActiveCfg = Release|Any CPU
{92D959F2-66B8-490A-BA33-DA4421EBC948}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -603,6 +617,7 @@ Global
{F8FD2D6A-DCD1-4A7B-B599-B728A12A1754} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{829D9A67-2D07-4CE6-86C0-59F2549B0CFA} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{0772E545-A674-4165-9469-E3D79D88A4A8} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{92D959F2-66B8-490A-BA33-DA4421EBC948} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D003597F-372F-4068-A2F0-353BE3C3B39A}

View File

@ -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

View File

@ -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();
}
}

View File

@ -128,6 +128,11 @@ namespace Microsoft.AspNetCore.Mvc.Testing
private void SetContentRoot(IWebHostBuilder builder)
{
if (SetContentRootFromSetting(builder))
{
return;
}
var metadataAttributes = GetContentRootMetadataAttributes(
typeof(TEntryPoint).Assembly.FullName,
typeof(TEntryPoint).Assembly.GetName().Name);
@ -161,6 +166,24 @@ namespace Microsoft.AspNetCore.Mvc.Testing
}
}
private static bool SetContentRootFromSetting(IWebHostBuilder builder)
{
// Attempt to look for TEST_CONTENTROOT_APPNAME in settings. This should result in looking for
// ASPNETCORE_TEST_CONTENTROOT_APPNAME environment variable.
var assemblyName = typeof(TEntryPoint).Assembly.GetName().Name;
var settingSuffix = assemblyName.ToUpperInvariant().Replace(".", "_");
var settingName = $"TEST_CONTENTROOT_{settingSuffix}";
var settingValue = builder.GetSetting(settingName);
if (settingValue == null)
{
return false;
}
builder.UseContentRoot(settingValue);
return true;
}
private WebApplicationFactoryContentRootAttribute[] GetContentRootMetadataAttributes(
string tEntryPointAssemblyFullName,
string tEntryPointAssemblyName)

View File

@ -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;
}
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -67,5 +67,16 @@ namespace FormatterWebSite
{
return Json(simpleTypePropertiesModel);
}
[HttpPost]
public IActionResult ValidationProviderAttribute([FromBody] ValidationProviderAttributeModel validationProviderAttributeModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok();
}
}
}

View File

@ -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()
};
}
}
}

View File

@ -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"});

View File

@ -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()
};
}
}
}

View File

@ -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>