Add support for model binding DateTime as UTC (#24893)
* Add support for model binding DateTime as UTC Fixes https://github.com/dotnet/aspnetcore/issues/11584 * Make test work in other TZs * Changes per PR comments * Cleanup unused exception code path, fix doc comments * Clean up usage of variables * Adjust logging to be consistent * Apply suggestions from code review
This commit is contained in:
parent
58a75925f7
commit
512a49c401
|
|
@ -63,6 +63,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
|
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
|
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options));
|
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options));
|
||||||
|
options.ModelBinderProviders.Add(new DateTimeModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
|
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
|
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
|
||||||
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
|
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
// 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.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IModelBinder"/> for <see cref="DateTime"/> and nullable <see cref="DateTime"/> models.
|
||||||
|
/// </summary>
|
||||||
|
public class DateTimeModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
private readonly DateTimeStyles _supportedStyles;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="DateTimeModelBinder"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="supportedStyles">The <see cref="DateTimeStyles"/>.</param>
|
||||||
|
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||||
|
public DateTimeModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
if (loggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
_supportedStyles = supportedStyles;
|
||||||
|
_logger = loggerFactory.CreateLogger<DateTimeModelBinder>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (bindingContext == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(bindingContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.AttemptingToBindModel(bindingContext);
|
||||||
|
|
||||||
|
var modelName = bindingContext.ModelName;
|
||||||
|
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
|
||||||
|
if (valueProviderResult == ValueProviderResult.None)
|
||||||
|
{
|
||||||
|
_logger.FoundNoValueInRequest(bindingContext);
|
||||||
|
|
||||||
|
// no entry
|
||||||
|
_logger.DoneAttemptingToBindModel(bindingContext);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelState = bindingContext.ModelState;
|
||||||
|
modelState.SetModelValue(modelName, valueProviderResult);
|
||||||
|
|
||||||
|
var metadata = bindingContext.ModelMetadata;
|
||||||
|
var type = metadata.UnderlyingOrModelType;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = valueProviderResult.FirstValue;
|
||||||
|
|
||||||
|
object model;
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
// Parse() method trims the value (with common DateTimeSyles) then throws if the result is empty.
|
||||||
|
model = null;
|
||||||
|
}
|
||||||
|
else if (type == typeof(DateTime))
|
||||||
|
{
|
||||||
|
model = DateTime.Parse(value, valueProviderResult.Culture, _supportedStyles);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// When converting value, a null model may indicate a failed conversion for an otherwise required
|
||||||
|
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
|
||||||
|
// current bindingContext. If not, an error is logged.
|
||||||
|
if (model == null && !metadata.IsReferenceOrNullableType)
|
||||||
|
{
|
||||||
|
modelState.TryAddModelError(
|
||||||
|
modelName,
|
||||||
|
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
|
||||||
|
valueProviderResult.ToString()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bindingContext.Result = ModelBindingResult.Success(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// Conversion failed.
|
||||||
|
modelState.TryAddModelError(modelName, exception, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.DoneAttemptingToBindModel(bindingContext);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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.Globalization;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IModelBinderProvider"/> for binding <see cref="DateTime" /> and nullable <see cref="DateTime"/> models.
|
||||||
|
/// </summary>
|
||||||
|
public class DateTimeModelBinderProvider : IModelBinderProvider
|
||||||
|
{
|
||||||
|
internal static readonly DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelType = context.Metadata.UnderlyingOrModelType;
|
||||||
|
if (modelType == typeof(DateTime))
|
||||||
|
{
|
||||||
|
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
||||||
|
return new DateTimeModelBinder(SupportedStyles, loggerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,7 +63,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var value = valueProviderResult.FirstValue;
|
var value = valueProviderResult.FirstValue;
|
||||||
var culture = valueProviderResult.Culture;
|
|
||||||
|
|
||||||
object model;
|
object model;
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
}
|
}
|
||||||
else if (type == typeof(decimal))
|
else if (type == typeof(decimal))
|
||||||
{
|
{
|
||||||
model = decimal.Parse(value, _supportedStyles, culture);
|
model = decimal.Parse(value, _supportedStyles, valueProviderResult.Culture);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var value = valueProviderResult.FirstValue;
|
var value = valueProviderResult.FirstValue;
|
||||||
var culture = valueProviderResult.Culture;
|
|
||||||
|
|
||||||
object model;
|
object model;
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
}
|
}
|
||||||
else if (type == typeof(double))
|
else if (type == typeof(double))
|
||||||
{
|
{
|
||||||
model = double.Parse(value, _supportedStyles, culture);
|
model = double.Parse(value, _supportedStyles, valueProviderResult.Culture);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var value = valueProviderResult.FirstValue;
|
var value = valueProviderResult.FirstValue;
|
||||||
var culture = valueProviderResult.Culture;
|
|
||||||
|
|
||||||
object model;
|
object model;
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
}
|
}
|
||||||
else if (type == typeof(float))
|
else if (type == typeof(float))
|
||||||
{
|
{
|
||||||
model = float.Parse(value, _supportedStyles, culture);
|
model = float.Parse(value, _supportedStyles, valueProviderResult.Culture);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
throw new ArgumentNullException(nameof(bindingContext));
|
throw new ArgumentNullException(nameof(bindingContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.AttemptingToBindModel(bindingContext);
|
||||||
|
|
||||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
if (valueProviderResult == ValueProviderResult.None)
|
if (valueProviderResult == ValueProviderResult.None)
|
||||||
{
|
{
|
||||||
|
|
@ -56,8 +58,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.AttemptingToBindModel(bindingContext);
|
|
||||||
|
|
||||||
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
|
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,9 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinderProvider.C
|
||||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder
|
||||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider
|
||||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider.ComplexTypeModelBinderProvider() -> void
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider.ComplexTypeModelBinderProvider() -> void
|
||||||
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder
|
||||||
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider
|
||||||
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider.DateTimeModelBinderProvider() -> void
|
||||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder
|
||||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder<TKey, TValue>
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder<TKey, TValue>
|
||||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinderProvider
|
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinderProvider
|
||||||
|
|
@ -1464,6 +1467,9 @@ virtual Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit
|
||||||
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.ComplexTypeModelBinder(System.Collections.Generic.IDictionary<Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder> propertyBinders, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.ComplexTypeModelBinder(System.Collections.Generic.IDictionary<Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder> propertyBinders, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
||||||
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.ComplexTypeModelBinder(System.Collections.Generic.IDictionary<Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder> propertyBinders, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, bool allowValidatingTopLevelNodes) -> void
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.ComplexTypeModelBinder(System.Collections.Generic.IDictionary<Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder> propertyBinders, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, bool allowValidatingTopLevelNodes) -> void
|
||||||
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder
|
||||||
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext bindingContext) -> System.Threading.Tasks.Task
|
||||||
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder.DateTimeModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
||||||
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder
|
||||||
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext bindingContext) -> System.Threading.Tasks.Task
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext bindingContext) -> System.Threading.Tasks.Task
|
||||||
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder.DecimalModelBinder(System.Globalization.NumberStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder.DecimalModelBinder(System.Globalization.NumberStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
||||||
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder<TKey, TValue>.DictionaryModelBinder(Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder keyBinder, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder valueBinder, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder<TKey, TValue>.DictionaryModelBinder(Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder keyBinder, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder valueBinder, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
{
|
||||||
|
public class DateTimeModelBinderProviderTest
|
||||||
|
{
|
||||||
|
private readonly DateTimeModelBinderProvider _provider = new DateTimeModelBinderProvider();
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(string))]
|
||||||
|
[InlineData(typeof(DateTimeOffset))]
|
||||||
|
[InlineData(typeof(DateTimeOffset?))]
|
||||||
|
[InlineData(typeof(TimeSpan))]
|
||||||
|
public void Create_ForNonDateTime_ReturnsNull(Type modelType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TestModelBinderProviderContext(modelType);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_ForDateTime_ReturnsBinder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TestModelBinderProviderContext(typeof(DateTime));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<DateTimeModelBinder>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Create_ForNullableDateTime_ReturnsBinder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TestModelBinderProviderContext(typeof(DateTime?));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _provider.GetBinder(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<DateTimeModelBinder>(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
// 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.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
{
|
||||||
|
public class DateTimeModelBinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task BindModel_ReturnsFailure_IfAttemptedValueCannotBeParsed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext();
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "some-value" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueCannotBeParsed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = "The value 'not a date' is not valid.";
|
||||||
|
var bindingContext = GetBindingContext();
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "not a date" },
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
Assert.False(bindingContext.ModelState.IsValid);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal(message, error.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BindModel_CreatesError_IfAttemptedValueCannotBeCompletelyParsed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext();
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("en-GB"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "2020-08-not-a-date" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal("The value '2020-08-not-a-date' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BindModel_ReturnsFailed_IfValueProviderEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(typeof(DateTime));
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result);
|
||||||
|
Assert.Empty(bindingContext.ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BindModel_NullableDatetime_ReturnsFailed_IfValueProviderEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(typeof(DateTime?));
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result);
|
||||||
|
Assert.Empty(bindingContext.ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" \t \r\n ")]
|
||||||
|
public async Task BindModel_CreatesError_IfTrimmedAttemptedValueIsEmpty_NonNullableDestination(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var message = $"The value '{value}' is invalid.";
|
||||||
|
var bindingContext = GetBindingContext();
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", value },
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bindingContext.Result.IsModelSet);
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
|
||||||
|
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||||
|
Assert.Equal(message, error.ErrorMessage, StringComparer.Ordinal);
|
||||||
|
Assert.Null(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" \t \r\n ")]
|
||||||
|
public async Task BindModel_ReturnsNull_IfTrimmedAttemptedValueIsEmpty_NullableDestination(string value)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext(typeof(DateTime?));
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", value }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(bindingContext.Result.Model);
|
||||||
|
var entry = Assert.Single(bindingContext.ModelState);
|
||||||
|
Assert.Equal("theModelName", entry.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(DateTime))]
|
||||||
|
[InlineData(typeof(DateTime?))]
|
||||||
|
public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid(Type type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = new DateTime(2019, 06, 14, 2, 30, 4, 0, DateTimeKind.Utc);
|
||||||
|
var bindingContext = GetBindingContext(type);
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "2019-06-14T02:30:04.0000000Z" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
var model = Assert.IsType<DateTime>(bindingContext.Result.Model);
|
||||||
|
Assert.Equal(expected, model);
|
||||||
|
Assert.Equal(DateTimeKind.Utc, model.Kind);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UsesSpecifiedStyleToParseModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = GetBindingContext();
|
||||||
|
var expected = DateTime.Parse("2019-06-14T02:30:04.0000000Z");
|
||||||
|
bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR"))
|
||||||
|
{
|
||||||
|
{ "theModelName", "2019-06-14T02:30:04.0000000Z" }
|
||||||
|
};
|
||||||
|
var binder = GetBinder(DateTimeStyles.AssumeLocal);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(bindingContext.Result.IsModelSet);
|
||||||
|
var model = Assert.IsType<DateTime>(bindingContext.Result.Model);
|
||||||
|
Assert.Equal(expected, model);
|
||||||
|
Assert.Equal(DateTimeKind.Local, model.Kind);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IModelBinder GetBinder(DateTimeStyles? dateTimeStyles = null)
|
||||||
|
{
|
||||||
|
return new DateTimeModelBinder(dateTimeStyles ?? DateTimeModelBinderProvider.SupportedStyles, NullLoggerFactory.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultModelBindingContext GetBindingContext(Type modelType = null)
|
||||||
|
{
|
||||||
|
modelType ??= typeof(DateTime);
|
||||||
|
return new DefaultModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(modelType),
|
||||||
|
ModelName = "theModelName",
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ValueProvider = new SimpleValueProvider() // empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result);
|
Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result);
|
||||||
Assert.Empty(bindingContext.ModelState);
|
Assert.Empty(bindingContext.ModelState);
|
||||||
Assert.Equal(2, sink.Writes.Count());
|
Assert.Equal(3, sink.Writes.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
|
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<FloatingPointTypeModelBinderProvider>(binder),
|
binder => Assert.IsType<FloatingPointTypeModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<EnumTypeModelBinderProvider>(binder),
|
binder => Assert.IsType<EnumTypeModelBinderProvider>(binder),
|
||||||
|
binder => Assert.IsType<DateTimeModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
|
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
|
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
|
||||||
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),
|
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
@ -229,6 +231,91 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture("en-GB", "en-GB")]
|
||||||
|
public async Task BindDateTimeParameter_WithData_GetsBound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameter = new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "Parameter1",
|
||||||
|
ParameterType = typeof(DateTime),
|
||||||
|
BindingInfo = new BindingInfo(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.QueryString = QueryString.Create("Parameter1", "2020-02-01");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
// ModelBindingResult
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
// Model
|
||||||
|
var model = Assert.IsType<DateTime>(modelBindingResult.Model);
|
||||||
|
Assert.Equal(new DateTime(2020, 02, 01, 0, 0, 0, DateTimeKind.Utc), model);
|
||||||
|
|
||||||
|
// ModelState
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
|
||||||
|
Assert.Single(modelState.Keys);
|
||||||
|
var key = Assert.Single(modelState.Keys);
|
||||||
|
Assert.Equal("Parameter1", key);
|
||||||
|
Assert.Equal("2020-02-01", modelState[key].AttemptedValue);
|
||||||
|
Assert.Equal("2020-02-01", modelState[key].RawValue);
|
||||||
|
Assert.Empty(modelState[key].Errors);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture("en-GB", "en-GB")]
|
||||||
|
public async Task BindDateTimeParameter_WithDataFromBody_GetsBound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "\"2020-02-01\"";
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameter = new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "Parameter1",
|
||||||
|
ParameterType = typeof(DateTime),
|
||||||
|
BindingInfo = new BindingInfo
|
||||||
|
{
|
||||||
|
BindingSource = BindingSource.Body,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
|
||||||
|
request.ContentType = "application/json";
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
// ModelBindingResult
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
// Model
|
||||||
|
var model = Assert.IsType<DateTime>(modelBindingResult.Model);
|
||||||
|
Assert.Equal(new DateTime(2020, 02, 01, 0, 0, 0, DateTimeKind.Utc), model);
|
||||||
|
|
||||||
|
// ModelState
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindParameter_WithMultipleValues_GetsBoundToFirstValue()
|
public async Task BindParameter_WithMultipleValues_GetsBoundToFirstValue()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue