-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Add support for model binding DateTime as UTC #24893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type is constructable right? So it isn't really unreachable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe because this uncommon? IDK, removed it because it didn't really add much value. |
||
} | ||
|
||
// 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); | ||
dougbu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
_logger.DoneAttemptingToBindModel(bindingContext); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be configurable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can create a binder provider that configures the model binder (it's constructable). It's the pattern we use for other types. |
||
|
||
/// <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; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any functional reason for inlining this or just code clarity? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #24893 (comment) |
||
} | ||
else | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,8 @@ public Task BindModelAsync(ModelBindingContext bindingContext) | |
throw new ArgumentNullException(nameof(bindingContext)); | ||
} | ||
|
||
_logger.AttemptingToBindModel(bindingContext); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need move the logging above the work in other binders? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of the ones that relied on the value provider, Simple type was the only outlier. |
||
|
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.