diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index a2b94da1188e..c12f5af2a381 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -63,6 +63,7 @@ public void Configure(MvcOptions options) 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()); diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateTimeModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateTimeModelBinder.cs new file mode 100644 index 000000000000..70f7403b064a --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateTimeModelBinder.cs @@ -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 +{ + /// + /// An for and nullable models. + /// + public class DateTimeModelBinder : IModelBinder + { + private readonly DateTimeStyles _supportedStyles; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public DateTimeModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _supportedStyles = supportedStyles; + _logger = loggerFactory.CreateLogger(); + } + + /// + 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; + } + } +} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateTimeModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateTimeModelBinderProvider.cs new file mode 100644 index 000000000000..aad9323f193d --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateTimeModelBinderProvider.cs @@ -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 +{ + /// + /// An for binding and nullable models. + /// + public class DateTimeModelBinderProvider : IModelBinderProvider + { + internal static readonly DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces; + + /// + 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(); + return new DateTimeModelBinder(SupportedStyles, loggerFactory); + } + + return null; + } + } +} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DecimalModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DecimalModelBinder.cs index b8f27116b568..83001eb70627 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DecimalModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DecimalModelBinder.cs @@ -63,7 +63,6 @@ public Task BindModelAsync(ModelBindingContext bindingContext) try { var value = valueProviderResult.FirstValue; - var culture = valueProviderResult.Culture; object model; if (string.IsNullOrWhiteSpace(value)) @@ -73,7 +72,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) } else if (type == typeof(decimal)) { - model = decimal.Parse(value, _supportedStyles, culture); + model = decimal.Parse(value, _supportedStyles, valueProviderResult.Culture); } else { diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DoubleModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DoubleModelBinder.cs index 64dd08301a4f..27e7417bd946 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DoubleModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DoubleModelBinder.cs @@ -63,7 +63,6 @@ public Task BindModelAsync(ModelBindingContext bindingContext) try { var value = valueProviderResult.FirstValue; - var culture = valueProviderResult.Culture; object model; if (string.IsNullOrWhiteSpace(value)) @@ -73,7 +72,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) } else if (type == typeof(double)) { - model = double.Parse(value, _supportedStyles, culture); + model = double.Parse(value, _supportedStyles, valueProviderResult.Culture); } else { diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FloatModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FloatModelBinder.cs index 09a150213b08..733d8e28c92c 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FloatModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FloatModelBinder.cs @@ -63,7 +63,6 @@ public Task BindModelAsync(ModelBindingContext bindingContext) try { var value = valueProviderResult.FirstValue; - var culture = valueProviderResult.Culture; object model; if (string.IsNullOrWhiteSpace(value)) @@ -73,7 +72,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) } else if (type == typeof(float)) { - model = float.Parse(value, _supportedStyles, culture); + model = float.Parse(value, _supportedStyles, valueProviderResult.Culture); } else { diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs index 8d9b1eeee99a..2bf1af977b55 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs @@ -46,6 +46,8 @@ public Task BindModelAsync(ModelBindingContext bindingContext) throw new ArgumentNullException(nameof(bindingContext)); } + _logger.AttemptingToBindModel(bindingContext); + var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult == ValueProviderResult.None) { @@ -56,8 +58,6 @@ public Task BindModelAsync(ModelBindingContext bindingContext) return Task.CompletedTask; } - _logger.AttemptingToBindModel(bindingContext); - bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); try diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index b122b73175a7..cff0b37416e6 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -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 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 propertyBinders, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void ~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.ComplexTypeModelBinder(System.Collections.Generic.IDictionary 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.DictionaryModelBinder(Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder keyBinder, Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder valueBinder, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateTimeModelBinderProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateTimeModelBinderProviderTest.cs new file mode 100644 index 000000000000..376d3b6765fc --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateTimeModelBinderProviderTest.cs @@ -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(result); + } + + [Fact] + public void Create_ForNullableDateTime_ReturnsBinder() + { + // Arrange + var context = new TestModelBinderProviderContext(typeof(DateTime?)); + + // Act + var result = _provider.GetBinder(context); + + // Assert + Assert.IsType(result); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateTimeModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateTimeModelBinderTest.cs new file mode 100644 index 000000000000..2fd87e551ab1 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateTimeModelBinderTest.cs @@ -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(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(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 + }; + } + } +} diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs index e50098b146f3..ee5c372cd095 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs @@ -194,7 +194,7 @@ public async Task BindModel_EmptyValueProviderResult_ReturnsFailedAndLogsSuccess // Assert Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result); Assert.Empty(bindingContext.ModelState); - Assert.Equal(2, sink.Writes.Count()); + Assert.Equal(3, sink.Writes.Count()); } [Theory] diff --git a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs index eb8d0933b48c..b0a740c1f945 100644 --- a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs +++ b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs @@ -58,6 +58,7 @@ public void Setup_SetsUpModelBinderProviders() binder => Assert.IsType(binder), binder => Assert.IsType(binder), binder => Assert.IsType(binder), + binder => Assert.IsType(binder), binder => Assert.IsType(binder), binder => Assert.IsType(binder), binder => Assert.IsType(binder), diff --git a/src/Mvc/test/Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index 62f3c3eb9fa8..634ba9bcedbb 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -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 @@ public async Task BindDecimalParameter_WithData_GetsBound() 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(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(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() {