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 FloatingPointTypeModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options));
|
||||
options.ModelBinderProviders.Add(new DateTimeModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
|
||||
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
|
||||
{
|
||||
var value = valueProviderResult.FirstValue;
|
||||
var culture = valueProviderResult.Culture;
|
||||
|
||||
object model;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
}
|
||||
else if (type == typeof(decimal))
|
||||
{
|
||||
model = decimal.Parse(value, _supportedStyles, culture);
|
||||
model = decimal.Parse(value, _supportedStyles, valueProviderResult.Culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
try
|
||||
{
|
||||
var value = valueProviderResult.FirstValue;
|
||||
var culture = valueProviderResult.Culture;
|
||||
|
||||
object model;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
}
|
||||
else if (type == typeof(double))
|
||||
{
|
||||
model = double.Parse(value, _supportedStyles, culture);
|
||||
model = double.Parse(value, _supportedStyles, valueProviderResult.Culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
try
|
||||
{
|
||||
var value = valueProviderResult.FirstValue;
|
||||
var culture = valueProviderResult.Culture;
|
||||
|
||||
object model;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
|
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
}
|
||||
else if (type == typeof(float))
|
||||
{
|
||||
model = float.Parse(value, _supportedStyles, culture);
|
||||
model = float.Parse(value, _supportedStyles, valueProviderResult.Culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
}
|
||||
|
||||
_logger.AttemptingToBindModel(bindingContext);
|
||||
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||
if (valueProviderResult == ValueProviderResult.None)
|
||||
{
|
||||
|
|
@ -56,8 +58,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_logger.AttemptingToBindModel(bindingContext);
|
||||
|
||||
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -463,6 +463,9 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinderProvider.C
|
|||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder
|
||||
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider
|
||||
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.DictionaryModelBinder<TKey, TValue>
|
||||
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, 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.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.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
|
||||
|
|
|
|||
|
|
@ -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.Equal(ModelBindingResult.Failed(), bindingContext.Result);
|
||||
Assert.Empty(bindingContext.ModelState);
|
||||
Assert.Equal(2, sink.Writes.Count());
|
||||
Assert.Equal(3, sink.Writes.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
binder => Assert.IsType<HeaderModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<FloatingPointTypeModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<EnumTypeModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<DateTimeModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<SimpleTypeModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<CancellationTokenModelBinderProvider>(binder),
|
||||
binder => Assert.IsType<ByteArrayModelBinderProvider>(binder),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
|
|
@ -229,6 +231,91 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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]
|
||||
public async Task BindParameter_WithMultipleValues_GetsBoundToFirstValue()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue