Skip to content

Commit fd50e5d

Browse files
committed
Add support for model binding DateTime as UTC
Fixes #11584
1 parent ede7d08 commit fd50e5d

File tree

8 files changed

+531
-0
lines changed

8 files changed

+531
-0
lines changed

src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public void Configure(MvcOptions options)
6363
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
6464
options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider());
6565
options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options));
66+
options.ModelBinderProviders.Add(new DateTimeModelBinderProvider());
6667
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
6768
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
6869
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
using System.Runtime.ExceptionServices;
7+
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
11+
{
12+
/// <summary>
13+
/// An <see cref="IModelBinder"/> for <see cref="DateTime"/> and nullable <see cref="DateTime"/> models.
14+
/// </summary>
15+
public class DateTimeModelBinder : IModelBinder
16+
{
17+
private readonly DateTimeStyles _supportedStyles;
18+
private readonly ILogger _logger;
19+
20+
/// <summary>
21+
/// Initializes a new instance of <see cref="DecimalModelBinder"/>.
22+
/// </summary>
23+
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
24+
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
25+
public DateTimeModelBinder(DateTimeStyles supportedStyles, ILoggerFactory loggerFactory)
26+
{
27+
if (loggerFactory == null)
28+
{
29+
throw new ArgumentNullException(nameof(loggerFactory));
30+
}
31+
32+
_supportedStyles = supportedStyles;
33+
_logger = loggerFactory.CreateLogger<DateTimeModelBinder>();
34+
}
35+
36+
/// <inheritdoc />
37+
public Task BindModelAsync(ModelBindingContext bindingContext)
38+
{
39+
if (bindingContext == null)
40+
{
41+
throw new ArgumentNullException(nameof(bindingContext));
42+
}
43+
44+
_logger.AttemptingToBindModel(bindingContext);
45+
46+
var modelName = bindingContext.ModelName;
47+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
48+
if (valueProviderResult == ValueProviderResult.None)
49+
{
50+
_logger.FoundNoValueInRequest(bindingContext);
51+
52+
// no entry
53+
_logger.DoneAttemptingToBindModel(bindingContext);
54+
return Task.CompletedTask;
55+
}
56+
57+
var modelState = bindingContext.ModelState;
58+
modelState.SetModelValue(modelName, valueProviderResult);
59+
60+
var metadata = bindingContext.ModelMetadata;
61+
var type = metadata.UnderlyingOrModelType;
62+
try
63+
{
64+
var value = valueProviderResult.FirstValue;
65+
var culture = valueProviderResult.Culture;
66+
67+
object model;
68+
if (string.IsNullOrWhiteSpace(value))
69+
{
70+
// Parse() method trims the value (with common DateTimeSyles) then throws if the result is empty.
71+
model = null;
72+
}
73+
else if (type == typeof(DateTime))
74+
{
75+
model = DateTime.Parse(value, culture, _supportedStyles);
76+
}
77+
else
78+
{
79+
// unreachable
80+
throw new NotSupportedException();
81+
}
82+
83+
// When converting value, a null model may indicate a failed conversion for an otherwise required
84+
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
85+
// current bindingContext. If not, an error is logged.
86+
if (model == null && !metadata.IsReferenceOrNullableType)
87+
{
88+
modelState.TryAddModelError(
89+
modelName,
90+
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
91+
valueProviderResult.ToString()));
92+
}
93+
else
94+
{
95+
bindingContext.Result = ModelBindingResult.Success(model);
96+
}
97+
}
98+
catch (Exception exception)
99+
{
100+
var isFormatException = exception is FormatException;
101+
if (!isFormatException && exception.InnerException != null)
102+
{
103+
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
104+
// this code in case a cursory review of the CoreFx code missed something.
105+
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
106+
}
107+
108+
modelState.TryAddModelError(modelName, exception, metadata);
109+
110+
// Conversion failed.
111+
}
112+
113+
_logger.DoneAttemptingToBindModel(bindingContext);
114+
return Task.CompletedTask;
115+
}
116+
}
117+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Globalization;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
10+
{
11+
/// <summary>
12+
/// An <see cref="IModelBinderProvider"/> for binding <see cref="DateTime" /> and nullable <see cref="DateTime"/> models.
13+
/// </summary>
14+
public class DateTimeModelBinderProvider : IModelBinderProvider
15+
{
16+
internal static readonly DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces;
17+
18+
/// <inheritdoc />
19+
public IModelBinder GetBinder(ModelBinderProviderContext context)
20+
{
21+
if (context == null)
22+
{
23+
throw new ArgumentNullException(nameof(context));
24+
}
25+
26+
var modelType = context.Metadata.UnderlyingOrModelType;
27+
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
28+
if (modelType == typeof(DateTime))
29+
{
30+
return new DateTimeModelBinder(SupportedStyles, loggerFactory);
31+
}
32+
33+
return null;
34+
}
35+
}
36+
}

src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,9 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinderProvider.C
463463
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder
464464
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider
465465
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider.ComplexTypeModelBinderProvider() -> void
466+
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder
467+
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider
468+
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider.DateTimeModelBinderProvider() -> void
466469
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder
467470
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinder<TKey, TValue>
468471
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinderProvider
@@ -1464,6 +1467,9 @@ virtual Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit
14641467
~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
14651468
~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
14661469
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder
1470+
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext bindingContext) -> System.Threading.Tasks.Task
1471+
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinder.DateTimeModelBinder(System.Globalization.DateTimeStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
1472+
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider.GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) -> Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder
14671473
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext bindingContext) -> System.Threading.Tasks.Task
14681474
~Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DecimalModelBinder.DecimalModelBinder(System.Globalization.NumberStyles supportedStyles, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) -> void
14691475
~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
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
9+
{
10+
public class DateTimeModelBinderProviderTest
11+
{
12+
private readonly DateTimeModelBinderProvider _provider = new DateTimeModelBinderProvider();
13+
14+
[Theory]
15+
[InlineData(typeof(string))]
16+
[InlineData(typeof(DateTimeOffset))]
17+
[InlineData(typeof(DateTimeOffset?))]
18+
[InlineData(typeof(TimeSpan))]
19+
public void Create_ForNonDateTime_ReturnsNull(Type modelType)
20+
{
21+
// Arrange
22+
var context = new TestModelBinderProviderContext(modelType);
23+
24+
// Act
25+
var result = _provider.GetBinder(context);
26+
27+
// Assert
28+
Assert.Null(result);
29+
}
30+
31+
[Fact]
32+
public void Create_ForDateTime_ReturnsBinder()
33+
{
34+
// Arrange
35+
var context = new TestModelBinderProviderContext(typeof(DateTime));
36+
37+
// Act
38+
var result = _provider.GetBinder(context);
39+
40+
// Assert
41+
Assert.IsType<DateTimeModelBinder>(result);
42+
}
43+
44+
[Fact]
45+
public void Create_ForNullableDateTime_ReturnsBinder()
46+
{
47+
// Arrange
48+
var context = new TestModelBinderProviderContext(typeof(DateTime?));
49+
50+
// Act
51+
var result = _provider.GetBinder(context);
52+
53+
// Assert
54+
Assert.IsType<DateTimeModelBinder>(result);
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)