Skip to content

Commit 47a6f75

Browse files
authored
Limit types that get converted to null for empty/null input in NullableConverter (#60498)
1 parent 6a0f132 commit 47a6f75

File tree

2 files changed

+102
-2
lines changed

2 files changed

+102
-2
lines changed

src/Components/Endpoints/src/FormMapping/Converters/NullableConverter.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public bool CanConvertSingleValue() => _nonNullableConverter is ISingleValueConv
1414

1515
public bool TryConvertValue(ref FormDataReader reader, string value, out T? result)
1616
{
17-
if (string.IsNullOrEmpty(value))
17+
if (string.IsNullOrEmpty(value) && IsSupportedUnderlyingType(typeof(T)))
1818
{
1919
// Form post sends empty string for a form field that does not have a value,
2020
// 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
4141
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
4242
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found)
4343
{
44-
// Donot call non-nullable converter's TryRead method, it will fail to parse empty
44+
// Do not call non-nullable converter's TryRead method, it will fail to parse empty
4545
// string. Call the TryConvertValue method above (similar to ParsableConverter) so
4646
// that it can handle the empty string correctly
4747
found = reader.TryGetValue(out var value);
@@ -55,4 +55,14 @@ internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMap
5555
return TryConvertValue(ref reader, value!, out result!);
5656
}
5757
}
58+
59+
private static bool IsSupportedUnderlyingType(Type type)
60+
{
61+
return Type.GetTypeCode(type) != TypeCode.Object || IsSupportedUnderlyingObjectType(type);
62+
}
63+
64+
private static bool IsSupportedUnderlyingObjectType(Type type)
65+
{
66+
return type == typeof(DateOnly) || type == typeof(TimeOnly) || type == typeof(DateTimeOffset);
67+
}
5868
}

src/Components/Endpoints/test/FormMapping/Converters/NullableConverterTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Globalization;
56
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
67
using Microsoft.Extensions.Primitives;
@@ -147,4 +148,93 @@ public void TryRead_ForDateOnlyReturnsFalseWithNullForBadDateValue()
147148
Assert.False(returnValue);
148149
Assert.Null(result);
149150
}
151+
152+
[Fact]
153+
public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForEmptyString()
154+
{
155+
var culture = CultureInfo.GetCultureInfo("en-US");
156+
157+
var nullableConverter = new NullableConverter<ParsableTestStruct>(new ParsableConverter<ParsableTestStruct>());
158+
var reader = new FormDataReader(default, culture, default);
159+
160+
var returnValue = nullableConverter.TryConvertValue(ref reader, string.Empty, out var result);
161+
162+
Assert.True(returnValue);
163+
Assert.NotNull(result);
164+
Assert.True(result.Value.WasEmptyOrNull);
165+
}
166+
167+
[Fact]
168+
public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForNull()
169+
{
170+
var culture = CultureInfo.GetCultureInfo("en-US");
171+
172+
var nullableConverter = new NullableConverter<ParsableTestStruct>(new ParsableConverter<ParsableTestStruct>());
173+
var reader = new FormDataReader(default, culture, default);
174+
175+
var returnValue = nullableConverter.TryConvertValue(ref reader, null, out var result);
176+
177+
Assert.True(returnValue);
178+
Assert.NotNull(result);
179+
Assert.True(result.Value.WasEmptyOrNull);
180+
}
181+
182+
[Fact]
183+
public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForGoodValue()
184+
{
185+
var culture = CultureInfo.GetCultureInfo("en-US");
186+
187+
var nullableConverter = new NullableConverter<ParsableTestStruct>(new ParsableConverter<ParsableTestStruct>());
188+
var reader = new FormDataReader(default, culture, default)
189+
{
190+
ErrorHandler = (_, __, ___) => { }
191+
};
192+
193+
var returnValue = nullableConverter.TryConvertValue(ref reader, "good value", out var result);
194+
195+
Assert.True(returnValue);
196+
Assert.False(result.Value.WasEmptyOrNull);
197+
}
198+
199+
[Fact]
200+
public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForBadValue()
201+
{
202+
var culture = CultureInfo.GetCultureInfo("en-US");
203+
204+
var nullableConverter = new NullableConverter<ParsableTestStruct>(new ParsableConverter<ParsableTestStruct>());
205+
var reader = new FormDataReader(default, culture, default)
206+
{
207+
ErrorHandler = (_, __, ___) => { }
208+
};
209+
210+
var returnValue = nullableConverter.TryConvertValue(ref reader, "bad value", out var result);
211+
212+
Assert.False(returnValue);
213+
}
214+
215+
private struct ParsableTestStruct : IParsable<ParsableTestStruct>
216+
{
217+
public bool WasEmptyOrNull { get; set; }
218+
219+
public static ParsableTestStruct Parse(string s, IFormatProvider provider) => throw new NotImplementedException();
220+
221+
public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out ParsableTestStruct result)
222+
{
223+
if (string.IsNullOrEmpty(s))
224+
{
225+
result = new ParsableTestStruct { WasEmptyOrNull = true };
226+
return true;
227+
}
228+
else if (s == "good value")
229+
{
230+
result = new ParsableTestStruct { WasEmptyOrNull = false };
231+
return true;
232+
}
233+
else
234+
{
235+
result = new();
236+
return false;
237+
}
238+
}
239+
}
150240
}

0 commit comments

Comments
 (0)