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:
parent
30b31d7086
commit
504f7f6856
|
|
@ -823,6 +823,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
{
|
||||
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 ValueProviderFactoryContext(Microsoft.AspNetCore.Mvc.ActionContext context) { }
|
||||
|
|
|
|||
|
|
@ -203,6 +203,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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)
|
||||
{
|
||||
EnsureMaxErrorsReachedRecorded();
|
||||
|
|
@ -311,9 +318,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1207,8 +1207,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
public void TryAddModelException_AddsErrorMessage_ForInputFormatterException()
|
||||
{
|
||||
// Arrange
|
||||
var expectedMessage = "This is an InputFormatterException";
|
||||
var dictionary = new ModelStateDictionary();
|
||||
var exception = new InputFormatterException("This is an InputFormatterException.");
|
||||
var exception = new InputFormatterException(expectedMessage);
|
||||
|
||||
// Act
|
||||
dictionary.TryAddModelException("key", exception);
|
||||
|
|
@ -1217,7 +1218,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
var entry = Assert.Single(dictionary);
|
||||
Assert.Equal("key", entry.Key);
|
||||
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]
|
||||
|
|
@ -1227,9 +1246,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
var expectedMessage = "This is an InputFormatterException";
|
||||
var dictionary = new ModelStateDictionary();
|
||||
|
||||
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
|
||||
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
|
||||
var provider = new DefaultModelMetadataProvider(compositeProvider, new OptionsAccessor());
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(typeof(int));
|
||||
|
||||
// Act
|
||||
|
|
@ -1242,6 +1259,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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]
|
||||
public void ModelStateDictionary_ClearEntriesThatMatchWithKey_NonEmptyKey()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2452,7 +2452,12 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -2526,7 +2531,12 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
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(
|
||||
model,
|
||||
prefix,
|
||||
|
|
@ -2565,7 +2575,12 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
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(
|
||||
model,
|
||||
prefix,
|
||||
|
|
@ -2693,7 +2708,12 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
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(
|
||||
model,
|
||||
modelType,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,12 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
|
||||
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;
|
||||
|
||||
for (var i = 0; i < parameters.Count; i++)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
var problemDetails = new ProblemDetails();
|
||||
|
||||
if (!reader.Read())
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException(Resources.UnexpectedJsonEnd);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
var problemDetails = new ValidationProblemDetails();
|
||||
|
||||
if (!reader.Read())
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException(Resources.UnexpectedJsonEnd);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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 />
|
||||
public virtual bool ContainsPrefix(string prefix)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
|
|
@ -32,10 +34,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, HttpRequest request)
|
||||
{
|
||||
var formCollection = await request.ReadFormAsync();
|
||||
if (formCollection.Files.Count > 0)
|
||||
IFormCollection form;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
|
|
@ -33,9 +36,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context)
|
||||
{
|
||||
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(
|
||||
BindingSource.Form,
|
||||
await request.ReadFormAsync(),
|
||||
form,
|
||||
CultureInfo.CurrentCulture);
|
||||
|
||||
context.ValueProviders.Add(valueProvider);
|
||||
|
|
|
|||
|
|
@ -513,4 +513,7 @@
|
|||
<data name="ApiConventions_Title_500" xml:space="preserve">
|
||||
<value>An error occured while processing your request.</value>
|
||||
</data>
|
||||
<data name="FailedToReadRequestForm" xml:space="preserve">
|
||||
<value>Failed to read the request form. {0}</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -2607,6 +2607,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
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]
|
||||
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 services = new ServiceCollection();
|
||||
|
|
@ -3056,11 +3081,11 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
|
|||
stringLocalizerFactory: null),
|
||||
};
|
||||
|
||||
valueProvider = valueProvider ?? new SimpleValueProvider();
|
||||
valueProvider ??= new SimpleValueProvider();
|
||||
var controllerContext = new ControllerContext()
|
||||
{
|
||||
HttpContext = httpContext,
|
||||
ValueProviderFactories = new[] { new SimpleValueProviderFactory(valueProvider), },
|
||||
ValueProviderFactories = new List<IValueProviderFactory> { new SimpleValueProviderFactory(valueProvider), },
|
||||
};
|
||||
|
||||
var binderFactory = new Mock<IModelBinderFactory>();
|
||||
|
|
|
|||
|
|
@ -1202,6 +1202,54 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
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)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ProblemDetailsConverterTest
|
||||
public class ProblemDetailsJsonConverterTest
|
||||
{
|
||||
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 converter = new ProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
reader.Read();
|
||||
|
||||
// Act
|
||||
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]
|
||||
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 converter = new ProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
reader.Read();
|
||||
|
||||
// Act
|
||||
var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions);
|
||||
|
|
@ -9,7 +9,7 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
public class ValidationProblemDetailsConverterTest
|
||||
public class ValidationProblemDetailsJsonConverterTest
|
||||
{
|
||||
private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().JsonSerializerOptions;
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
"\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}";
|
||||
var converter = new ValidationProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
reader.Read();
|
||||
|
||||
// Act
|
||||
var problemDetails = converter.Read(ref reader, typeof(ValidationProblemDetails), JsonSerializerOptions);
|
||||
|
|
@ -65,12 +66,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
var title = "Not found";
|
||||
var status = 404;
|
||||
var traceId = "|37dd3dd5-4a9619f953c40a16.";
|
||||
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}";
|
||||
var converter = new ProblemDetailsJsonConverter();
|
||||
var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," +
|
||||
"\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}";
|
||||
var converter = new ValidationProblemDetailsJsonConverter();
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
reader.Read();
|
||||
|
||||
// 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(title, problemDetails.Title);
|
||||
|
|
@ -82,6 +85,56 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
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]
|
||||
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]
|
||||
|
|
@ -55,7 +55,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
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++)
|
||||
{
|
||||
var property = properties[i];
|
||||
|
|
@ -131,7 +136,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
|
||||
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++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1335,7 +1335,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -1407,7 +1413,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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(
|
||||
model,
|
||||
prefix,
|
||||
|
|
@ -1445,7 +1456,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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(
|
||||
model,
|
||||
prefix,
|
||||
|
|
@ -1570,7 +1586,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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(
|
||||
model,
|
||||
modelType,
|
||||
|
|
|
|||
|
|
@ -222,7 +222,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +299,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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(
|
||||
model,
|
||||
name,
|
||||
|
|
@ -332,7 +342,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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(
|
||||
model,
|
||||
name,
|
||||
|
|
@ -457,7 +472,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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(
|
||||
model,
|
||||
modelType,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
var handlerMethodInfo = type.GetMethod(method);
|
||||
|
|
|
|||
|
|
@ -1739,6 +1739,35 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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]
|
||||
public void UrlHelperIsSet()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1817,6 +1817,35 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ using System.Net.Http.Headers;
|
|||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using BasicWebSite.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
|
|
@ -444,7 +443,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
var response = await Client.GetStringAsync(url);
|
||||
|
||||
// Assert
|
||||
var result = JsonConvert.DeserializeObject<Product>(response);
|
||||
var result = JsonSerializer.Deserialize<Product>(response, TestJsonSerializerOptionsProvider.Options);
|
||||
Assert.Equal(10, result.SampleInt);
|
||||
}
|
||||
|
||||
|
|
@ -458,7 +457,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
var response = await Client.GetStringAsync(url);
|
||||
|
||||
// Assert
|
||||
var result = JsonConvert.DeserializeObject<Product[]>(response);
|
||||
var result = JsonSerializer.Deserialize<Product[]>(response, TestJsonSerializerOptionsProvider.Options);
|
||||
Assert.Equal(2, result.Length);
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +476,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
// Act
|
||||
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[]
|
||||
{
|
||||
"BasicWebSite",
|
||||
|
|
@ -548,7 +547,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
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(10, data.Id);
|
||||
|
|
@ -571,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
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.Null(data.IdFromRoute);
|
||||
|
|
@ -593,7 +592,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var data = JsonConvert.DeserializeObject<BindPropertyControllerData>(content);
|
||||
var data = JsonSerializer.Deserialize<BindPropertyControllerData>(content, TestJsonSerializerOptionsProvider.Options);
|
||||
|
||||
Assert.Null(data.BindNeverProperty);
|
||||
}
|
||||
|
|
@ -628,6 +627,27 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
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 string Name { get; set; }
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ namespace BasicWebSite.Controllers
|
|||
[HttpPost]
|
||||
public IActionResult Product(Product product)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return ValidationProblem();
|
||||
}
|
||||
|
||||
return RedirectToAction();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue