From 5474e5df9ad70aaaf0a9ee8f9d69a2a3d5c09fd5 Mon Sep 17 00:00:00 2001 From: Nick Stanton Date: Mon, 21 Nov 2022 15:27:32 -0800 Subject: [PATCH 1/4] Add TimeOnly and DateOnly bind availability --- .../Infrastructure/MvcCoreMvcOptionsSetup.cs | 2 + .../Binders/DateOnlyModelBinder.cs | 104 ++++++++++++++++++ .../Binders/DateOnlyModelBinderProvider.cs | 36 ++++++ .../Binders/TimeOnlyModelBinder.cs | 104 ++++++++++++++++++ .../Binders/TimeOnlyModelBinderProvider.cs | 36 ++++++ src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt | 12 ++ 6 files changed, 294 insertions(+) create mode 100644 src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs create mode 100644 src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs create mode 100644 src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs create mode 100644 src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index 5ae0d2c9105e..3be747c018ac 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -74,6 +74,8 @@ public void Configure(MvcOptions options) options.ModelBinderProviders.Add(new ArrayModelBinderProvider()); options.ModelBinderProviders.Add(new CollectionModelBinderProvider()); options.ModelBinderProviders.Add(new ComplexObjectModelBinderProvider()); + options.ModelBinderProviders.Add(new DateOnlyModelBinderProvider()); + options.ModelBinderProviders.Add(new TimeOnlyModelBinderProvider()); // Set up filters options.Filters.Add(new UnsupportedContentTypeFilter()); diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs new file mode 100644 index 000000000000..c5b9a4fb7fe7 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +/// +/// An for and nullable models. +/// +public class DateOnlyModelBinder : IModelBinder +{ + private readonly DateTimeStyles _supportedStyles; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public DateOnlyModelBinder(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(DateOnly)) + { + model = DateOnly.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/DateOnlyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs new file mode 100644 index 000000000000..0a05994977f7 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +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 DateOnlyModelBinderProvider : IModelBinderProvider +{ + internal const 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(DateOnly)) + { + var loggerFactory = context.Services.GetRequiredService(); + return new DateOnlyModelBinder(SupportedStyles, loggerFactory); + } + + return null; + } +} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs new file mode 100644 index 000000000000..14aca55f1c9b --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Globalization; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +/// +/// An for and nullable models. +/// +public class TimeOnlyModelBinder : IModelBinder +{ + private readonly DateTimeStyles _supportedStyles; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public TimeOnlyModelBinder(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(TimeOnly)) + { + model = TimeOnly.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/TimeOnlyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs new file mode 100644 index 000000000000..9343b3821ba8 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +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 TimeOnlyModelBinderProvider : IModelBinderProvider +{ + internal const 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(TimeOnly)) + { + var loggerFactory = context.Services.GetRequiredService(); + return new TimeOnlyModelBinder(SupportedStyles, loggerFactory); + } + + return null; + } +} diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt index 53c83cd826af..536b41335e0f 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt @@ -1139,6 +1139,12 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinderProvider.G 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.DateOnlyModelBinder +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder.DateOnlyModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider.DateOnlyModelBinderProvider() -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder 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 @@ -1206,6 +1212,12 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinder.SimpleTypeMo Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider.SimpleTypeModelBinderProvider() -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder.TimeOnlyModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider.TimeOnlyModelBinderProvider() -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior.Never = 1 -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior.Optional = 0 -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior From cff11c5d858831de18eb2e9dafab389cc57a9be9 Mon Sep 17 00:00:00 2001 From: Nick Stanton Date: Tue, 22 Nov 2022 14:23:58 -0800 Subject: [PATCH 2/4] Add test coverage for binders and binderproviders --- .../Binders/DateOnlyModelBinderProvider.cs | 2 +- .../Binders/TimeOnlyModelBinderProvider.cs | 2 +- .../DateOnlyModelBinderProviderTest.cs | 54 +++++ .../Binders/DateOnlyModelBinderTest.cs | 216 ++++++++++++++++++ .../TimeOnlyModelBinderProviderTest.cs | 54 +++++ .../Binders/TimeOnlyModelBinderTest.cs | 216 ++++++++++++++++++ src/Mvc/Mvc/test/MvcOptionsSetupTest.cs | 4 +- 7 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderProviderTest.cs create mode 100644 src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderTest.cs create mode 100644 src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderProviderTest.cs create mode 100644 src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderTest.cs diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs index 0a05994977f7..f7deafcb6a7c 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; /// public class DateOnlyModelBinderProvider : IModelBinderProvider { - internal const DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces; + internal const DateTimeStyles SupportedStyles = DateTimeStyles.AllowWhiteSpaces; /// public IModelBinder? GetBinder(ModelBinderProviderContext context) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs index 9343b3821ba8..f2e370442a72 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; /// public class TimeOnlyModelBinderProvider : IModelBinderProvider { - internal const DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces; + internal const DateTimeStyles SupportedStyles = DateTimeStyles.AllowWhiteSpaces; /// public IModelBinder? GetBinder(ModelBinderProviderContext context) diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderProviderTest.cs new file mode 100644 index 000000000000..98e1dfa5d293 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderProviderTest.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +public class DateOnlyModelBinderProviderTest +{ + private readonly DateOnlyModelBinderProvider _provider = new DateOnlyModelBinderProvider(); + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(DateTimeOffset?))] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateTime?))] + [InlineData(typeof(TimeSpan))] + public void Create_ForNonDateOnly_ReturnsNull(Type modelType) + { + // Arrange + var context = new TestModelBinderProviderContext(modelType); + + // Act + var result = _provider.GetBinder(context); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Create_ForDateOnly_ReturnsBinder() + { + // Arrange + var context = new TestModelBinderProviderContext(typeof(DateOnly)); + + // Act + var result = _provider.GetBinder(context); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void Create_ForNullableDateOnly_ReturnsBinder() + { + // Arrange + var context = new TestModelBinderProviderContext(typeof(DateOnly?)); + + // Act + var result = _provider.GetBinder(context); + + // Assert + Assert.IsType(result); + } +} diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderTest.cs new file mode 100644 index 000000000000..c6db62a58be5 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/DateOnlyModelBinderTest.cs @@ -0,0 +1,216 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +public class DateOnlyModelBinderTest +{ + [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(DateOnly)); + var binder = GetBinder(); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result); + Assert.Empty(bindingContext.ModelState); + } + + [Fact] + public async Task BindModel_NullableDateOnly_ReturnsFailed_IfValueProviderEmpty() + { + // Arrange + var bindingContext = GetBindingContext(typeof(DateOnly?)); + 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(DateOnly?)); + 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(DateOnly))] + [InlineData(typeof(DateOnly?))] + public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid(Type type) + { + // Arrange + var expected = new DateOnly(2019, 06, 14); + var bindingContext = GetBindingContext(type); + bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR")) + { + { "theModelName", "2019-06-14" } + }; + 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.True(bindingContext.ModelState.ContainsKey("theModelName")); + } + + [Fact] + public async Task UsesSpecifiedStyleToParseModel() + { + // Arrange + var bindingContext = GetBindingContext(); + var expected = DateOnly.Parse("2019-06-14", CultureInfo.InvariantCulture); + bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR")) + { + { "theModelName", " 2019-06-14" } + }; + var binder = GetBinder(DateTimeStyles.AllowLeadingWhite); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + var model = Assert.IsType(bindingContext.Result.Model); + Assert.Equal(expected, model); + Assert.True(bindingContext.ModelState.ContainsKey("theModelName")); + } + + private IModelBinder GetBinder(DateTimeStyles? dateTimeStyles = null) + { + return new DateOnlyModelBinder(dateTimeStyles ?? DateOnlyModelBinderProvider.SupportedStyles, NullLoggerFactory.Instance); + } + + private static DefaultModelBindingContext GetBindingContext(Type modelType = null) + { + modelType ??= typeof(DateOnly); + 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/TimeOnlyModelBinderProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderProviderTest.cs new file mode 100644 index 000000000000..6eaf5bd39215 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderProviderTest.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +public class TimeOnlyModelBinderProviderTest +{ + private readonly TimeOnlyModelBinderProvider _provider = new TimeOnlyModelBinderProvider(); + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(DateTimeOffset?))] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateTime?))] + [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_ForTimeOnly_ReturnsBinder() + { + // Arrange + var context = new TestModelBinderProviderContext(typeof(TimeOnly)); + + // Act + var result = _provider.GetBinder(context); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void Create_ForNullableTimeOnly_ReturnsBinder() + { + // Arrange + var context = new TestModelBinderProviderContext(typeof(TimeOnly?)); + + // Act + var result = _provider.GetBinder(context); + + // Assert + Assert.IsType(result); + } +} diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderTest.cs new file mode 100644 index 000000000000..eb2fab9cf7ed --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/TimeOnlyModelBinderTest.cs @@ -0,0 +1,216 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging.Abstractions; +using System.Globalization; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders; + +public class TimeOnlyModelBinderTest +{ + [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 time' is not valid."; + var bindingContext = GetBindingContext(); + bindingContext.ValueProvider = new SimpleValueProvider + { + { "theModelName", "not a time" }, + }; + 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", "11:05:08-not-a-time" } + }; + 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 '11:05:08-not-a-time' is not valid.", error.ErrorMessage, StringComparer.Ordinal); + Assert.Null(error.Exception); + } + + [Fact] + public async Task BindModel_ReturnsFailed_IfValueProviderEmpty() + { + // Arrange + var bindingContext = GetBindingContext(typeof(TimeOnly)); + var binder = GetBinder(); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.Equal(ModelBindingResult.Failed(), bindingContext.Result); + Assert.Empty(bindingContext.ModelState); + } + + [Fact] + public async Task BindModel_NullableTimeOnly_ReturnsFailed_IfValueProviderEmpty() + { + // Arrange + var bindingContext = GetBindingContext(typeof(TimeOnly?)); + 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(TimeOnly?)); + 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(TimeOnly))] + [InlineData(typeof(TimeOnly?))] + public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid(Type type) + { + // Arrange + var expected = new TimeOnly(2, 30, 4, 0); + var bindingContext = GetBindingContext(type); + bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR")) + { + { "theModelName", "02:30:04.0000000" } + }; + 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.True(bindingContext.ModelState.ContainsKey("theModelName")); + } + + [Fact] + public async Task UsesSpecifiedStyleToParseModel() + { + // Arrange + var bindingContext = GetBindingContext(); + var expected = TimeOnly.Parse("02:30:04.0000000", CultureInfo.InvariantCulture); + bindingContext.ValueProvider = new SimpleValueProvider(new CultureInfo("fr-FR")) + { + { "theModelName", " 02:30:04.0000000" } + }; + var binder = GetBinder(DateTimeStyles.AllowLeadingWhite); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + var model = Assert.IsType(bindingContext.Result.Model); + Assert.Equal(expected, model); + Assert.True(bindingContext.ModelState.ContainsKey("theModelName")); + } + + private IModelBinder GetBinder(DateTimeStyles? dateTimeStyles = null) + { + return new TimeOnlyModelBinder(dateTimeStyles ?? TimeOnlyModelBinderProvider.SupportedStyles, NullLoggerFactory.Instance); + } + + private static DefaultModelBindingContext GetBindingContext(Type modelType = null) + { + modelType ??= typeof(TimeOnly); + return new DefaultModelBindingContext + { + ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(modelType), + ModelName = "theModelName", + ModelState = new ModelStateDictionary(), + ValueProvider = new SimpleValueProvider() // empty + }; + } +} diff --git a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs index 79c83b8ec421..cdb3a37f6072 100644 --- a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs +++ b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs @@ -62,7 +62,9 @@ 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)); } [Fact] From 50beacfc9a3796afb6baac2c88904ccba69d3a77 Mon Sep 17 00:00:00 2001 From: Nick Stanton Date: Mon, 28 Nov 2022 11:48:41 -0700 Subject: [PATCH 3/4] Move new API to unshipped file --- src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt | 12 ------------ src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt index 536b41335e0f..53c83cd826af 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Shipped.txt @@ -1139,12 +1139,6 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinderProvider.G 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.DateOnlyModelBinder -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder.DateOnlyModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider.DateOnlyModelBinderProvider() -> void -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder 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 @@ -1212,12 +1206,6 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinder.SimpleTypeMo Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider.SimpleTypeModelBinderProvider() -> void -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder.TimeOnlyModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider.TimeOnlyModelBinderProvider() -> void -Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior.Never = 1 -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior.Optional = 0 -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingBehavior diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index c2c8fdf41a5a..17e0567d0d1e 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -36,3 +36,15 @@ virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions Microsoft.AspNetCore.Mvc.EmptyResult! *REMOVED*virtual Microsoft.AspNetCore.Mvc.ModelBinding.DefaultPropertyFilterProvider.PropertyIncludeExpressions.get -> System.Collections.Generic.IEnumerable!>!>? virtual Microsoft.AspNetCore.Mvc.ModelBinding.DefaultPropertyFilterProvider.PropertyIncludeExpressions.get -> System.Collections.Generic.IEnumerable!>!>? +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinder.DateOnlyModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider.DateOnlyModelBinderProvider() -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateOnlyModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinder.TimeOnlyModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider.TimeOnlyModelBinderProvider() -> void +Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TimeOnlyModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext! context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? From ed90b058fe349fec522eb40196f6902088539080 Mon Sep 17 00:00:00 2001 From: Nick Stanton Date: Mon, 28 Nov 2022 13:27:40 -0700 Subject: [PATCH 4/4] Replace null exception throwing --- .../src/ModelBinding/Binders/DateOnlyModelBinder.cs | 10 ++-------- .../Binders/DateOnlyModelBinderProvider.cs | 5 +---- .../src/ModelBinding/Binders/TimeOnlyModelBinder.cs | 10 ++-------- .../Binders/TimeOnlyModelBinderProvider.cs | 5 +---- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs index c5b9a4fb7fe7..20dfc90ccbd1 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinder.cs @@ -23,10 +23,7 @@ public class DateOnlyModelBinder : IModelBinder /// The . public DateOnlyModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } + ArgumentNullException.ThrowIfNull(loggerFactory, nameof(loggerFactory)); _supportedStyles = supportedStyles; _logger = loggerFactory.CreateLogger(); @@ -35,10 +32,7 @@ public DateOnlyModelBinder(DateTimeStyles supportedStyles, ILoggerFactory logger /// public Task BindModelAsync(ModelBindingContext bindingContext) { - if (bindingContext == null) - { - throw new ArgumentNullException(nameof(bindingContext)); - } + ArgumentNullException.ThrowIfNull(bindingContext, nameof(bindingContext)); _logger.AttemptingToBindModel(bindingContext); diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs index f7deafcb6a7c..2614abdc76a2 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/DateOnlyModelBinderProvider.cs @@ -19,10 +19,7 @@ public class DateOnlyModelBinderProvider : IModelBinderProvider /// public IModelBinder? GetBinder(ModelBinderProviderContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + ArgumentNullException.ThrowIfNull(context, nameof(context)); var modelType = context.Metadata.UnderlyingOrModelType; if (modelType == typeof(DateOnly)) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs index 14aca55f1c9b..e3abfa29b123 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinder.cs @@ -23,10 +23,7 @@ public class TimeOnlyModelBinder : IModelBinder /// The . public TimeOnlyModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } + ArgumentNullException.ThrowIfNull(loggerFactory, nameof(loggerFactory)); _supportedStyles = supportedStyles; _logger = loggerFactory.CreateLogger(); @@ -35,10 +32,7 @@ public TimeOnlyModelBinder(DateTimeStyles supportedStyles, ILoggerFactory logger /// public Task BindModelAsync(ModelBindingContext bindingContext) { - if (bindingContext == null) - { - throw new ArgumentNullException(nameof(bindingContext)); - } + ArgumentNullException.ThrowIfNull(bindingContext, nameof(bindingContext)); _logger.AttemptingToBindModel(bindingContext); diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs index f2e370442a72..8a994c8d56ac 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/TimeOnlyModelBinderProvider.cs @@ -19,10 +19,7 @@ public class TimeOnlyModelBinderProvider : IModelBinderProvider /// public IModelBinder? GetBinder(ModelBinderProviderContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + ArgumentNullException.ThrowIfNull(context, nameof(context)); var modelType = context.Metadata.UnderlyingOrModelType; if (modelType == typeof(TimeOnly))