Converting DateTime.MinValue to sqlDateTime's minimum value (#20019)

* Converting DateTime.MinValue to sqlDateTime's minimum value

* Changing code to be a bit less hacky

* Changing hard coded value to a variable based on SqlDateTime

* Removing unused code

* Moving date converter logic to DateTimePropertyEditor.cs

* Replacing tests with proper version

* Removing unused import

* Removing unused imports again

* Creating new logic, to ensure formatting is more precise.

* Rewriting tests to be more precise and include testing on odd format separators

* Used parsing to determine timeonly date picker data type configuration format.
Fixed casing on key for data type configuration format.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Nicklas Kramer
2025-09-09 09:47:49 +02:00
committed by GitHub
parent 0712fda069
commit fa39908899
2 changed files with 133 additions and 1 deletions

View File

@@ -1,8 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Data.SqlTypes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -25,8 +33,91 @@ public class DateTimePropertyEditor : DataEditor
/// <inheritdoc />
protected override IDataValueEditor CreateValueEditor()
{
IDataValueEditor editor = base.CreateValueEditor();
DateTimePropertyValueEditor editor = DataValueEditorFactory.Create<DateTimePropertyValueEditor>(Attribute!);
editor.Validators.Add(new DateTimeValidator());
return editor;
}
/// <summary>
/// Provides a value editor for the datetime property editor.
/// </summary>
internal sealed class DateTimePropertyValueEditor : DataValueEditor
{
/// <summary>
/// The key used to retrieve the date format from the data type configuration.
/// </summary>
internal const string DateTypeConfigurationFormatKey = "format";
/// <summary>
/// Initializes a new instance of the <see cref="DateTimePropertyValueEditor"/> class.
/// </summary>
public DateTimePropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
}
/// <inheritdoc/>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (editorValue.Value is null)
{
return base.FromEditor(editorValue, currentValue);
}
if (TryGetConfiguredDateTimeFormat(editorValue, out string? format) is false)
{
return base.FromEditor(editorValue, currentValue);
}
if (IsTimeOnlyFormat(format) is false)
{
return base.FromEditor(editorValue, currentValue);
}
// We have a time-only format, so we need to ensure the date part is valid for SQL Server, so we can persist
// without error.
// If we have a date part that is less than the minimum date, we need to adjust it to be the minimum date.
Attempt<object?> dateConvertAttempt = editorValue.Value.TryConvertTo(typeof(DateTime?));
if (dateConvertAttempt.Success is false || dateConvertAttempt.Result is null)
{
return base.FromEditor(editorValue, currentValue);
}
var dateTimeValue = (DateTime)dateConvertAttempt.Result;
int yearValue = dateTimeValue.Year > SqlDateTime.MinValue.Value.Year
? dateTimeValue.Year
: SqlDateTime.MinValue.Value.Year;
return new DateTime(yearValue, dateTimeValue.Month, dateTimeValue.Day, dateTimeValue.Hour, dateTimeValue.Minute, dateTimeValue.Second);
}
private static bool TryGetConfiguredDateTimeFormat(ContentPropertyData editorValue, [NotNullWhen(true)] out string? format)
{
if (editorValue.DataTypeConfiguration is not Dictionary<string, object> dataTypeConfigurationDictionary)
{
format = null;
return false;
}
KeyValuePair<string, object> keyValuePair = dataTypeConfigurationDictionary
.FirstOrDefault(kvp => kvp.Key is "format");
format = keyValuePair.Value as string;
return string.IsNullOrWhiteSpace(format) is false;
}
private static bool IsTimeOnlyFormat(string format)
{
DateTime testDate = DateTime.UtcNow;
var testDateFormatted = testDate.ToString(format);
if (DateTime.TryParseExact(testDateFormatted, format, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out DateTime parsedDate) is false)
{
return false;
}
return parsedDate.Year == 1 && parsedDate.Month == 1 && parsedDate.Day == 1;
}
}
}

View File

@@ -0,0 +1,41 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PropertyEditors;
public class DateTimePropertyEditorTests
{
// Various time formats with years below the minimum, so we expect to increase the date to the minimum supported by SQL Server.
[TestCase("01/01/0001 10:00", "01/01/1753 10:00", "hh:mm")]
[TestCase("01/01/0001 10:00", "01/01/1753 10:00", "HH:mm")]
[TestCase("01/01/0001 10:00", "01/01/1753 10:00", "hh mm")]
[TestCase("10/10/1000 10:00", "10/10/1753 10:00", "hh:mm:ss")]
[TestCase("10/10/1000 10:00", "10/10/1753 10:00", "hh-mm-ss")]
// Time format with year above the minimum, so we expect to not convert.
[TestCase("01/01/2000 10:00", "01/01/2000 10:00", "HH:mm")]
// Date formats, so we don't convert even if the year is below the minimum.
[TestCase("01/01/0001 10:00", "01/01/0001 10:00", "dd-MM-yyyy hh:mm")]
[TestCase("01/01/0001 10:00", "01/01/0001 10:00", "dd-MM-yyyy")]
[TestCase("01/01/0001 10:00", "01/01/0001 10:00", "yyyy-MM-d")]
public void Time_Only_Format_Ensures_DateTime_Can_Be_Persisted(DateTime actualDateTime, DateTime expectedDateTime, string format)
{
var dateTimePropertyEditor = new DateTimePropertyEditor.DateTimePropertyValueEditor(
Mock.Of<IShortStringHelper>(),
Mock.Of<IJsonSerializer>(),
Mock.Of<IIOHelper>(),
new DataEditorAttribute("Alias") { ValueType = ValueTypes.DateTime.ToString() });
Dictionary<string, object> dictionary = new Dictionary<string, object> { { DateTimePropertyEditor.DateTimePropertyValueEditor.DateTypeConfigurationFormatKey, format } };
ContentPropertyData propertyData = new ContentPropertyData(actualDateTime, dictionary);
var value = (DateTime)dateTimePropertyEditor.FromEditor(propertyData, null);
Assert.AreEqual(expectedDateTime, value);
}
}