diff --git a/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs b/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs index 0285ded3902a..205f114b732f 100644 --- a/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs +++ b/src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs @@ -14,7 +14,7 @@ public bool CanConvertSingleValue() => _nonNullableConverter is ISingleValueConv public bool TryConvertValue(ref FormDataReader reader, string value, out T? result) { - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(value) && IsSupportedUnderlyingType(typeof(T))) { // Form post sends empty string for a form field that does not have a value, // in case of nullable value types, that should be treated as null and @@ -41,7 +41,7 @@ public bool TryConvertValue(ref FormDataReader reader, string value, out T? resu [RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)] internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found) { - // Donot call non-nullable converter's TryRead method, it will fail to parse empty + // Do not call non-nullable converter's TryRead method, it will fail to parse empty // string. Call the TryConvertValue method above (similar to ParsableConverter) so // that it can handle the empty string correctly found = reader.TryGetValue(out var value); @@ -55,4 +55,14 @@ internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMap return TryConvertValue(ref reader, value!, out result!); } } + + private static bool IsSupportedUnderlyingType(Type type) + { + return Type.GetTypeCode(type) != TypeCode.Object || IsSupportedUnderlyingObjectType(type); + } + + private static bool IsSupportedUnderlyingObjectType(Type type) + { + return type == typeof(DateOnly) || type == typeof(TimeOnly) || type == typeof(DateTimeOffset); + } } diff --git a/src/Components/Endpoints/test/FormMapping/Converters/NullableConverterTests.cs b/src/Components/Endpoints/test/FormMapping/Converters/NullableConverterTests.cs index f091fad51b3c..7294fedcb6cd 100644 --- a/src/Components/Endpoints/test/FormMapping/Converters/NullableConverterTests.cs +++ b/src/Components/Endpoints/test/FormMapping/Converters/NullableConverterTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.AspNetCore.Components.Endpoints.FormMapping; using Microsoft.Extensions.Primitives; @@ -147,4 +148,93 @@ public void TryRead_ForDateOnlyReturnsFalseWithNullForBadDateValue() Assert.False(returnValue); Assert.Null(result); } + + [Fact] + public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForEmptyString() + { + var culture = CultureInfo.GetCultureInfo("en-US"); + + var nullableConverter = new NullableConverter(new ParsableConverter()); + var reader = new FormDataReader(default, culture, default); + + var returnValue = nullableConverter.TryConvertValue(ref reader, string.Empty, out var result); + + Assert.True(returnValue); + Assert.NotNull(result); + Assert.True(result.Value.WasEmptyOrNull); + } + + [Fact] + public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForNull() + { + var culture = CultureInfo.GetCultureInfo("en-US"); + + var nullableConverter = new NullableConverter(new ParsableConverter()); + var reader = new FormDataReader(default, culture, default); + + var returnValue = nullableConverter.TryConvertValue(ref reader, null, out var result); + + Assert.True(returnValue); + Assert.NotNull(result); + Assert.True(result.Value.WasEmptyOrNull); + } + + [Fact] + public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForGoodValue() + { + var culture = CultureInfo.GetCultureInfo("en-US"); + + var nullableConverter = new NullableConverter(new ParsableConverter()); + var reader = new FormDataReader(default, culture, default) + { + ErrorHandler = (_, __, ___) => { } + }; + + var returnValue = nullableConverter.TryConvertValue(ref reader, "good value", out var result); + + Assert.True(returnValue); + Assert.False(result.Value.WasEmptyOrNull); + } + + [Fact] + public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForBadValue() + { + var culture = CultureInfo.GetCultureInfo("en-US"); + + var nullableConverter = new NullableConverter(new ParsableConverter()); + var reader = new FormDataReader(default, culture, default) + { + ErrorHandler = (_, __, ___) => { } + }; + + var returnValue = nullableConverter.TryConvertValue(ref reader, "bad value", out var result); + + Assert.False(returnValue); + } + + private struct ParsableTestStruct : IParsable + { + public bool WasEmptyOrNull { get; set; } + + public static ParsableTestStruct Parse(string s, IFormatProvider provider) => throw new NotImplementedException(); + + public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out ParsableTestStruct result) + { + if (string.IsNullOrEmpty(s)) + { + result = new ParsableTestStruct { WasEmptyOrNull = true }; + return true; + } + else if (s == "good value") + { + result = new ParsableTestStruct { WasEmptyOrNull = false }; + return true; + } + else + { + result = new(); + return false; + } + } + } }