Produce a ModelState error when reading the form throws (#14994)

* Introduce ValueProviderException analogous to InputFormatterException
* Record ValueProviderException as a model state error
* Fixup bug in reading ProblemDetails \ ValidationProblemDetails using the converter

Fixes https://github.com/aspnet/AspNetCore/issues/10291
This commit is contained in:
Pranav K 2019-10-15 16:46:35 -07:00 committed by GitHub
parent 30b31d7086
commit 504f7f6856
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 538 additions and 44 deletions

View File

@ -823,6 +823,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
public TooManyModelErrorsException(string message) { } public TooManyModelErrorsException(string message) { }
} }
public sealed partial class ValueProviderException : System.Exception
{
public ValueProviderException(string message) { }
public ValueProviderException(string message, System.Exception innerException) { }
}
public partial class ValueProviderFactoryContext public partial class ValueProviderFactoryContext
{ {
public ValueProviderFactoryContext(Microsoft.AspNetCore.Mvc.ActionContext context) { } public ValueProviderFactoryContext(Microsoft.AspNetCore.Mvc.ActionContext context) { }

View File

@ -203,6 +203,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(exception)); throw new ArgumentNullException(nameof(exception));
} }
if ((exception is InputFormatterException || exception is ValueProviderException)
&& !string.IsNullOrEmpty(exception.Message))
{
// InputFormatterException, ValueProviderException is a signal that the message is safe to expose to clients
return TryAddModelError(key, exception.Message);
}
if (ErrorCount >= MaxAllowedErrors - 1) if (ErrorCount >= MaxAllowedErrors - 1)
{ {
EnsureMaxErrorsReachedRecorded(); EnsureMaxErrorsReachedRecorded();
@ -311,9 +318,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return TryAddModelError(key, errorMessage); return TryAddModelError(key, errorMessage);
} }
else if (exception is InputFormatterException && !string.IsNullOrEmpty(exception.Message)) else if ((exception is InputFormatterException || exception is ValueProviderException)
&& !string.IsNullOrEmpty(exception.Message))
{ {
// InputFormatterException is a signal that the message is safe to expose to clients // InputFormatterException, ValueProviderException is a signal that the message is safe to expose to clients
return TryAddModelError(key, exception.Message); return TryAddModelError(key, exception.Message);
} }

View File

@ -0,0 +1,33 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// Exception thrown by <see cref="IValueProviderFactory"/> when the input is unable to be read.
/// </summary>
public sealed class ValueProviderException : Exception
{
/// <summary>
/// Initializes a new instance of <see cref="ValueProviderException"/> with the specified <paramref name="message"/>.
/// </summary>
/// <param name="message">The exception message.</param>
public ValueProviderException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of <see cref="ValueProviderException"/> with the specified <paramref name="message"/> and
/// inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public ValueProviderException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -1207,8 +1207,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public void TryAddModelException_AddsErrorMessage_ForInputFormatterException() public void TryAddModelException_AddsErrorMessage_ForInputFormatterException()
{ {
// Arrange // Arrange
var expectedMessage = "This is an InputFormatterException";
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
var exception = new InputFormatterException("This is an InputFormatterException."); var exception = new InputFormatterException(expectedMessage);
// Act // Act
dictionary.TryAddModelException("key", exception); dictionary.TryAddModelException("key", exception);
@ -1217,7 +1218,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var entry = Assert.Single(dictionary); var entry = Assert.Single(dictionary);
Assert.Equal("key", entry.Key); Assert.Equal("key", entry.Key);
var error = Assert.Single(entry.Value.Errors); var error = Assert.Single(entry.Value.Errors);
Assert.Same(exception, error.Exception); Assert.Equal(expectedMessage, error.ErrorMessage);
}
[Fact]
public void TryAddModelException_AddsErrorMessage_ForValueProviderException()
{
// Arrange
var expectedMessage = "This is an ValueProviderException";
var dictionary = new ModelStateDictionary();
var exception = new ValueProviderException(expectedMessage);
// Act
dictionary.TryAddModelException("key", exception);
// Assert
var entry = Assert.Single(dictionary);
Assert.Equal("key", entry.Key);
var error = Assert.Single(entry.Value.Errors);
Assert.Equal(expectedMessage, error.ErrorMessage);
} }
[Fact] [Fact]
@ -1227,9 +1246,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var expectedMessage = "This is an InputFormatterException"; var expectedMessage = "This is an InputFormatterException";
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
var bindingMetadataProvider = new DefaultBindingMetadataProvider(); var provider = new EmptyModelMetadataProvider();
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
var provider = new DefaultModelMetadataProvider(compositeProvider, new OptionsAccessor());
var metadata = provider.GetMetadataForType(typeof(int)); var metadata = provider.GetMetadataForType(typeof(int));
// Act // Act
@ -1242,6 +1259,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(expectedMessage, error.ErrorMessage); Assert.Equal(expectedMessage, error.ErrorMessage);
} }
[Fact]
public void ModelStateDictionary_AddsErrorMessage_ForValueProviderException()
{
// Arrange
var expectedMessage = "This is an ValueProviderException";
var dictionary = new ModelStateDictionary();
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(typeof(int));
// Act
dictionary.TryAddModelError("key", new ValueProviderException(expectedMessage), metadata);
// Assert
var entry = Assert.Single(dictionary);
Assert.Equal("key", entry.Key);
var error = Assert.Single(entry.Value.Errors);
Assert.Equal(expectedMessage, error.ErrorMessage);
}
[Fact] [Fact]
public void ModelStateDictionary_ClearEntriesThatMatchWithKey_NonEmptyKey() public void ModelStateDictionary_ClearEntriesThatMatchWithKey_NonEmptyKey()
{ {

View File

@ -2452,7 +2452,12 @@ namespace Microsoft.AspNetCore.Mvc
throw new ArgumentNullException(nameof(prefix)); throw new ArgumentNullException(nameof(prefix));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(ControllerContext); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(ControllerContext, ControllerContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await TryUpdateModelAsync(model, prefix, valueProvider); return await TryUpdateModelAsync(model, prefix, valueProvider);
} }
@ -2526,7 +2531,12 @@ namespace Microsoft.AspNetCore.Mvc
throw new ArgumentNullException(nameof(includeExpressions)); throw new ArgumentNullException(nameof(includeExpressions));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(ControllerContext); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(ControllerContext, ControllerContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
prefix, prefix,
@ -2565,7 +2575,12 @@ namespace Microsoft.AspNetCore.Mvc
throw new ArgumentNullException(nameof(propertyFilter)); throw new ArgumentNullException(nameof(propertyFilter));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(ControllerContext); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(ControllerContext, ControllerContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
prefix, prefix,
@ -2693,7 +2708,12 @@ namespace Microsoft.AspNetCore.Mvc
throw new ArgumentNullException(nameof(modelType)); throw new ArgumentNullException(nameof(modelType));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(ControllerContext); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(ControllerContext, ControllerContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
modelType, modelType,

View File

@ -59,7 +59,12 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments) async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments)
{ {
var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(controllerContext, controllerContext.ValueProviderFactories);
if (!success)
{
return;
}
var parameters = actionDescriptor.Parameters; var parameters = actionDescriptor.Parameters;
for (var i = 0; i < parameters.Count; i++) for (var i = 0; i < parameters.Count; i++)

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
var problemDetails = new ProblemDetails(); var problemDetails = new ProblemDetails();
if (!reader.Read()) if (reader.TokenType != JsonTokenType.StartObject)
{ {
throw new JsonException(Resources.UnexpectedJsonEnd); throw new JsonException(Resources.UnexpectedJsonEnd);
} }

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
var problemDetails = new ValidationProblemDetails(); var problemDetails = new ValidationProblemDetails();
if (!reader.Read()) if (reader.TokenType != JsonTokenType.StartObject)
{ {
throw new JsonException(Resources.UnexpectedJsonEnd); throw new JsonException(Resources.UnexpectedJsonEnd);
} }

View File

@ -80,6 +80,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new CompositeValueProvider(valueProviderFactoryContext.ValueProviders); return new CompositeValueProvider(valueProviderFactoryContext.ValueProviders);
} }
internal static async ValueTask<(bool success, CompositeValueProvider valueProvider)> TryCreateAsync(
ActionContext actionContext,
IList<IValueProviderFactory> factories)
{
try
{
var valueProvider = await CreateAsync(actionContext, factories);
return (true, valueProvider);
}
catch (ValueProviderException exception)
{
actionContext.ModelState.TryAddModelException(key: string.Empty, exception);
return (false, null);
}
}
/// <inheritdoc /> /// <inheritdoc />
public virtual bool ContainsPrefix(string prefix) public virtual bool ContainsPrefix(string prefix)
{ {

View File

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
@ -32,10 +34,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, HttpRequest request) private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, HttpRequest request)
{ {
var formCollection = await request.ReadFormAsync(); IFormCollection form;
if (formCollection.Files.Count > 0)
try
{ {
var valueProvider = new FormFileValueProvider(formCollection.Files); form = await request.ReadFormAsync();
}
catch (InvalidDataException ex)
{
throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
}
if (form.Files.Count > 0)
{
var valueProvider = new FormFileValueProvider(form.Files);
context.ValueProviders.Add(valueProvider); context.ValueProviders.Add(valueProvider);
} }
} }

View File

@ -3,7 +3,10 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
@ -33,9 +36,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) private static async Task AddValueProviderAsync(ValueProviderFactoryContext context)
{ {
var request = context.ActionContext.HttpContext.Request; var request = context.ActionContext.HttpContext.Request;
IFormCollection form;
try
{
form = await request.ReadFormAsync();
}
catch (InvalidDataException ex)
{
throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
}
var valueProvider = new FormValueProvider( var valueProvider = new FormValueProvider(
BindingSource.Form, BindingSource.Form,
await request.ReadFormAsync(), form,
CultureInfo.CurrentCulture); CultureInfo.CurrentCulture);
context.ValueProviders.Add(valueProvider); context.ValueProviders.Add(valueProvider);

View File

@ -513,4 +513,7 @@
<data name="ApiConventions_Title_500" xml:space="preserve"> <data name="ApiConventions_Title_500" xml:space="preserve">
<value>An error occured while processing your request.</value> <value>An error occured while processing your request.</value>
</data> </data>
<data name="FailedToReadRequestForm" xml:space="preserve">
<value>Failed to read the request form. {0}</value>
</data>
</root> </root>

View File

@ -2607,6 +2607,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.NotEqual(0, binder.BindModelCount); Assert.NotEqual(0, binder.BindModelCount);
} }
[Fact]
public async Task TryUpdateModel_ReturnsFalse_IfValueProviderFactoryThrows()
{
// Arrange
var modelName = "mymodel";
var valueProviderFactory = new Mock<IValueProviderFactory>();
valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
.Throws(new ValueProviderException("some error"));
var controller = GetController(new StubModelBinder());
controller.ControllerContext.ValueProviderFactories.Add(valueProviderFactory.Object);
var model = new MyModel();
// Act
var result = await controller.TryUpdateModelAsync(model, modelName);
// Assert
Assert.False(result);
var modelState = Assert.Single(controller.ModelState);
Assert.Empty(modelState.Key);
var error = Assert.Single(modelState.Value.Errors);
Assert.Equal("some error", error.ErrorMessage);
}
[Fact] [Fact]
public async Task TryUpdateModel_PropertyFilterOverload_UsesPassedArguments() public async Task TryUpdateModel_PropertyFilterOverload_UsesPassedArguments()
{ {
@ -3037,7 +3062,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
}); });
} }
private static ControllerBase GetController(IModelBinder binder, IValueProvider valueProvider) private static ControllerBase GetController(IModelBinder binder, IValueProvider valueProvider = null)
{ {
var metadataProvider = new EmptyModelMetadataProvider(); var metadataProvider = new EmptyModelMetadataProvider();
var services = new ServiceCollection(); var services = new ServiceCollection();
@ -3056,11 +3081,11 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
stringLocalizerFactory: null), stringLocalizerFactory: null),
}; };
valueProvider = valueProvider ?? new SimpleValueProvider(); valueProvider ??= new SimpleValueProvider();
var controllerContext = new ControllerContext() var controllerContext = new ControllerContext()
{ {
HttpContext = httpContext, HttpContext = httpContext,
ValueProviderFactories = new[] { new SimpleValueProviderFactory(valueProvider), }, ValueProviderFactories = new List<IValueProviderFactory> { new SimpleValueProviderFactory(valueProvider), },
}; };
var binderFactory = new Mock<IModelBinderFactory>(); var binderFactory = new Mock<IModelBinderFactory>();

View File

@ -1202,6 +1202,54 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
Assert.Equal(250.0, transferInfo.Amount); Assert.Equal(250.0, transferInfo.Amount);
} }
[Fact]
public async Task BinderDelegateRecordsErrorWhenValueProviderThrowsValueProviderException()
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor()
{
BoundProperties = new List<ParameterDescriptor>(),
Parameters = new[] { new ParameterDescriptor { Name = "name", ParameterType = typeof(string) } },
};
var modelMetadataProvider = new EmptyModelMetadataProvider();
var modelBinderProvider = Mock.Of<IModelBinderProvider>();
var factory = TestModelBinderFactory.CreateDefault(modelBinderProvider);
var modelValidatorProvider = Mock.Of<IModelValidatorProvider>();
var parameterBinder = new ParameterBinder(
new EmptyModelMetadataProvider(),
factory,
GetObjectValidator(modelMetadataProvider, modelValidatorProvider),
_optionsAccessor,
NullLoggerFactory.Instance);
var valueProviderFactory = new Mock<IValueProviderFactory>();
valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
.Throws(new ValueProviderException("Some error"));
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ValueProviderFactories.Add(valueProviderFactory.Object);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelState = controllerContext.ModelState;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor,
_options);
await binderDelegate(controllerContext, new TestController(), arguments);
// Assert
var entry = Assert.Single(modelState);
Assert.Empty(entry.Key);
var error = Assert.Single(entry.Value.Errors);
Assert.Equal("Some error", error.ErrorMessage);
}
private static ControllerContext GetControllerContext(ControllerActionDescriptor descriptor = null) private static ControllerContext GetControllerContext(ControllerActionDescriptor descriptor = null)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();

View File

@ -8,7 +8,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
public class ProblemDetailsConverterTest public class ProblemDetailsJsonConverterTest
{ {
private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions; private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions;
@ -41,6 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}"; var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}";
var converter = new ProblemDetailsJsonConverter(); var converter = new ProblemDetailsJsonConverter();
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
reader.Read();
// Act // Act
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);
@ -59,6 +60,35 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
}); });
} }
[Fact]
public void Read_UsingJsonSerializerWorks()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var title = "Not found";
var status = 404;
var detail = "Product not found";
var instance = "http://example.com/products/14";
var traceId = "|37dd3dd5-4a9619f953c40a16.";
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}";
// Act
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, JsonSerializerOptions);
Assert.Equal(type, problemDetails.Type);
Assert.Equal(title, problemDetails.Title);
Assert.Equal(status, problemDetails.Status);
Assert.Equal(instance, problemDetails.Instance);
Assert.Equal(detail, problemDetails.Detail);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("traceId", kvp.Key);
Assert.Equal(traceId, kvp.Value.ToString());
});
}
[Fact] [Fact]
public void Read_WithSomeMissingValues_Works() public void Read_WithSomeMissingValues_Works()
{ {
@ -70,6 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}"; var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}";
var converter = new ProblemDetailsJsonConverter(); var converter = new ProblemDetailsJsonConverter();
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
reader.Read();
// Act // Act
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);

View File

@ -9,7 +9,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
public class ValidationProblemDetailsConverterTest public class ValidationProblemDetailsJsonConverterTest
{ {
private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions; private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions;
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
"\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}";
var converter = new ValidationProblemDetailsJsonConverter(); var converter = new ValidationProblemDetailsJsonConverter();
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
reader.Read();
// Act // Act
var problemDetails = converter.Read(ref reader, typeof(ValidationProblemDetails), JsonSerializerOptions); var problemDetails = converter.Read(ref reader, typeof(ValidationProblemDetails), JsonSerializerOptions);
@ -65,12 +66,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
var title = "Not found"; var title = "Not found";
var status = 404; var status = 404;
var traceId = "|37dd3dd5-4a9619f953c40a16."; var traceId = "|37dd3dd5-4a9619f953c40a16.";
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}"; var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," +
var converter = new ProblemDetailsJsonConverter(); "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}";
var converter = new ValidationProblemDetailsJsonConverter();
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
reader.Read();
// Act // Act
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); var problemDetails = converter.Read(ref reader, typeof(ValidationProblemDetails), JsonSerializerOptions);
Assert.Equal(type, problemDetails.Type); Assert.Equal(type, problemDetails.Type);
Assert.Equal(title, problemDetails.Title); Assert.Equal(title, problemDetails.Title);
@ -82,6 +85,56 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Assert.Equal("traceId", kvp.Key); Assert.Equal("traceId", kvp.Key);
Assert.Equal(traceId, kvp.Value.ToString()); Assert.Equal(traceId, kvp.Value.ToString());
}); });
Assert.Collection(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("key0", kvp.Key);
Assert.Equal(new[] { "error0" }, kvp.Value);
},
kvp =>
{
Assert.Equal("key1", kvp.Key);
Assert.Equal(new[] { "error1", "error2" }, kvp.Value);
});
}
[Fact]
public void ReadUsingJsonSerializerWorks()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var title = "Not found";
var status = 404;
var traceId = "|37dd3dd5-4a9619f953c40a16.";
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," +
"\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}";
// Act
var problemDetails = JsonSerializer.Deserialize<ValidationProblemDetails>(json, JsonSerializerOptions);
Assert.Equal(type, problemDetails.Type);
Assert.Equal(title, problemDetails.Title);
Assert.Equal(status, problemDetails.Status);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("traceId", kvp.Key);
Assert.Equal(traceId, kvp.Value.ToString());
});
Assert.Collection(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("key0", kvp.Key);
Assert.Equal(new[] { "error0" }, kvp.Value);
},
kvp =>
{
Assert.Equal("key1", kvp.Key);
Assert.Equal(new[] { "error1", "error2" }, kvp.Value);
});
} }
[Fact] [Fact]

View File

@ -55,7 +55,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
async Task Bind(PageContext pageContext, object instance) async Task Bind(PageContext pageContext, object instance)
{ {
var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(pageContext, pageContext.ValueProviderFactories);
if (!success)
{
return;
}
for (var i = 0; i < properties.Count; i++) for (var i = 0; i < properties.Count; i++)
{ {
var property = properties[i]; var property = properties[i];
@ -131,7 +136,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
async Task Bind(PageContext pageContext, IDictionary<string, object> arguments) async Task Bind(PageContext pageContext, IDictionary<string, object> arguments)
{ {
var valueProvider = await CompositeValueProvider.CreateAsync(pageContext, pageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(pageContext, pageContext.ValueProviderFactories);
if (!success)
{
return;
}
for (var i = 0; i < parameterBindingInfo.Length; i++) for (var i = 0; i < parameterBindingInfo.Length; i++)
{ {

View File

@ -1335,7 +1335,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
{ {
throw new ArgumentNullException(nameof(prefix)); throw new ArgumentNullException(nameof(prefix));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories);
var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await TryUpdateModelAsync(model, prefix, valueProvider); return await TryUpdateModelAsync(model, prefix, valueProvider);
} }
@ -1407,7 +1413,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(includeExpressions)); throw new ArgumentNullException(nameof(includeExpressions));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
prefix, prefix,
@ -1445,7 +1456,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(propertyFilter)); throw new ArgumentNullException(nameof(propertyFilter));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
prefix, prefix,
@ -1570,7 +1586,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(modelType)); throw new ArgumentNullException(nameof(modelType));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
modelType, modelType,

View File

@ -222,7 +222,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(name)); throw new ArgumentNullException(nameof(name));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await TryUpdateModelAsync(model, name, valueProvider); return await TryUpdateModelAsync(model, name, valueProvider);
} }
@ -294,7 +299,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(includeExpressions)); throw new ArgumentNullException(nameof(includeExpressions));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
name, name,
@ -332,7 +342,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(propertyFilter)); throw new ArgumentNullException(nameof(propertyFilter));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
name, name,
@ -457,7 +472,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
throw new ArgumentNullException(nameof(modelType)); throw new ArgumentNullException(nameof(modelType));
} }
var valueProvider = await CompositeValueProvider.CreateAsync(PageContext, PageContext.ValueProviderFactories); var (success, valueProvider) = await CompositeValueProvider.TryCreateAsync(PageContext, PageContext.ValueProviderFactories);
if (!success)
{
return false;
}
return await ModelBindingHelper.TryUpdateModelAsync( return await ModelBindingHelper.TryUpdateModelAsync(
model, model,
modelType, modelType,

View File

@ -709,6 +709,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
}); });
} }
[Fact]
public async Task FactoryRecordsErrorWhenValueProviderThrowsValueProviderException()
{
// Arrange
var type = typeof(PageModelWithExecutors);
var actionDescriptor = GetActionDescriptorWithHandlerMethod(type, nameof(PageModelWithExecutors.OnGet));
// Act
var parameterBinder = new TestParameterBinder(new Dictionary<string, object>()
{
{ "id", "value" },
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelBinderFactory = TestModelBinderFactory.CreateDefault();
var factory = PageBinderFactory.CreateHandlerBinder(
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
actionDescriptor,
actionDescriptor.HandlerMethods[0],
_options);
var pageContext = GetPageContext();
var page = new PageWithProperty
{
PageContext = pageContext,
};
var valueProviderFactory = new Mock<IValueProviderFactory>();
valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
.Throws(new ValueProviderException("Some error"));
pageContext.ValueProviderFactories.Add(valueProviderFactory.Object);
var model = new PageModelWithExecutors();
var arguments = new Dictionary<string, object>();
// Act
await factory(page.PageContext, arguments);
// Assert
var modelState = pageContext.ModelState;
var entry = Assert.Single(modelState);
Assert.Empty(entry.Key);
var error = Assert.Single(entry.Value.Errors);
Assert.Equal("Some error", error.ErrorMessage);
}
private static CompiledPageActionDescriptor GetActionDescriptorWithHandlerMethod(Type type, string method) private static CompiledPageActionDescriptor GetActionDescriptorWithHandlerMethod(Type type, string method)
{ {
var handlerMethodInfo = type.GetMethod(method); var handlerMethodInfo = type.GetMethod(method);

View File

@ -1739,6 +1739,35 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
Assert.Same(viewData, pageModel.ViewData); Assert.Same(viewData, pageModel.ViewData);
} }
[Fact]
public async Task TryUpdateModel_ReturnsFalse_IfValueProviderFactoryThrows()
{
// Arrange
var valueProviderFactory = new Mock<IValueProviderFactory>();
valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
.Throws(new ValueProviderException("some error"));
var pageModel = new TestPageModel
{
PageContext = new PageContext
{
ValueProviderFactories = new[] { valueProviderFactory.Object },
}
};
var model = new object();
// Act
var result = await pageModel.TryUpdateModelAsync(model);
// Assert
Assert.False(result);
var modelState = Assert.Single(pageModel.ModelState);
Assert.Empty(modelState.Key);
var error = Assert.Single(modelState.Value.Errors);
Assert.Equal("some error", error.ErrorMessage);
}
[Fact] [Fact]
public void UrlHelperIsSet() public void UrlHelperIsSet()
{ {

View File

@ -1817,6 +1817,35 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
Assert.Same(viewData, result.ViewData); Assert.Same(viewData, result.ViewData);
} }
[Fact]
public async Task TryUpdateModel_ReturnsFalse_IfValueProviderFactoryThrows()
{
// Arrange
var valueProviderFactory = new Mock<IValueProviderFactory>();
valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
.Throws(new ValueProviderException("some error"));
var pageModel = new TestPage
{
PageContext = new PageContext
{
ValueProviderFactories = new[] { valueProviderFactory.Object },
}
};
var model = new object();
// Act
var result = await pageModel.TryUpdateModelAsync(model);
// Assert
Assert.False(result);
var modelState = Assert.Single(pageModel.ModelState);
Assert.Empty(modelState.Key);
var error = Assert.Single(modelState.Value.Errors);
Assert.Equal("some error", error.ErrorMessage);
}
public static IEnumerable<object[]> RedirectTestData public static IEnumerable<object[]> RedirectTestData
{ {
get get

View File

@ -9,8 +9,7 @@ using System.Net.Http.Headers;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using BasicWebSite.Models; using BasicWebSite.Models;
using Newtonsoft.Json; using System.Text.Json;
using Newtonsoft.Json.Linq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -444,7 +443,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await Client.GetStringAsync(url); var response = await Client.GetStringAsync(url);
// Assert // Assert
var result = JsonConvert.DeserializeObject<Product>(response); var result = JsonSerializer.Deserialize<Product>(response, TestJsonSerializerOptionsProvider.Options);
Assert.Equal(10, result.SampleInt); Assert.Equal(10, result.SampleInt);
} }
@ -458,7 +457,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await Client.GetStringAsync(url); var response = await Client.GetStringAsync(url);
// Assert // Assert
var result = JsonConvert.DeserializeObject<Product[]>(response); var result = JsonSerializer.Deserialize<Product[]>(response, TestJsonSerializerOptionsProvider.Options);
Assert.Equal(2, result.Length); Assert.Equal(2, result.Length);
} }
@ -477,7 +476,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{ {
// Act // Act
var response = await Client.GetStringAsync("Home/GetAssemblyPartData"); var response = await Client.GetStringAsync("Home/GetAssemblyPartData");
var assemblyParts = JsonConvert.DeserializeObject<IList<string>>(response); var assemblyParts = JsonSerializer.Deserialize<IList<string>>(response, TestJsonSerializerOptionsProvider.Options);
var expected = new[] var expected = new[]
{ {
"BasicWebSite", "BasicWebSite",
@ -548,7 +547,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<BindPropertyControllerData>(content); var data = JsonSerializer.Deserialize<BindPropertyControllerData>(content, TestJsonSerializerOptionsProvider.Options);
Assert.Equal("TestName", data.Name); Assert.Equal("TestName", data.Name);
Assert.Equal(10, data.Id); Assert.Equal(10, data.Id);
@ -571,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<BindPropertyControllerData>(content); var data = JsonSerializer.Deserialize<BindPropertyControllerData>(content, TestJsonSerializerOptionsProvider.Options);
Assert.Equal(10, data.Id); Assert.Equal(10, data.Id);
Assert.Null(data.IdFromRoute); Assert.Null(data.IdFromRoute);
@ -593,7 +592,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK); await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<BindPropertyControllerData>(content); var data = JsonSerializer.Deserialize<BindPropertyControllerData>(content, TestJsonSerializerOptionsProvider.Options);
Assert.Null(data.BindNeverProperty); Assert.Null(data.BindNeverProperty);
} }
@ -628,6 +627,27 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("OnGetTestName", content); Assert.Equal("OnGetTestName", content);
} }
[Fact]
public async Task InvalidForm_ResultsInModelError()
{
// Arrange
var request = new HttpRequestMessage(HttpMethod.Post, "/Home/Product");
request.Content = new MultipartFormDataContent();
var response = await Client.SendAsync(request);
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonSerializer.Deserialize<ValidationProblemDetails>(content, TestJsonSerializerOptionsProvider.Options);
Assert.Collection(
problemDetails.Errors,
kvp =>
{
Assert.Empty(kvp.Key);
Assert.Equal("Failed to read the request form. Form section has invalid Content-Disposition value: ", string.Join(" ", kvp.Value));
});
}
public class BindPropertyControllerData public class BindPropertyControllerData
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@ -120,6 +120,11 @@ namespace BasicWebSite.Controllers
[HttpPost] [HttpPost]
public IActionResult Product(Product product) public IActionResult Product(Product product)
{ {
if (!ModelState.IsValid)
{
return ValidationProblem();
}
return RedirectToAction(); return RedirectToAction();
} }