Skip to content

Commit 3dd098b

Browse files
committed
Limit types that get converted to null for empty/null input in NullableConverter
1 parent e215183 commit 3dd098b

File tree

2 files changed

+103
-2
lines changed

2 files changed

+103
-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: 91 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,94 @@ 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+
183+
[Fact]
184+
public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForGoodValue()
185+
{
186+
var culture = CultureInfo.GetCultureInfo("en-US");
187+
188+
var nullableConverter = new NullableConverter<ParsableTestStruct>(new ParsableConverter<ParsableTestStruct>());
189+
var reader = new FormDataReader(default, culture, default)
190+
{
191+
ErrorHandler = (_, __, ___) => { }
192+
};
193+
194+
var returnValue = nullableConverter.TryConvertValue(ref reader, "good value", out var result);
195+
196+
Assert.True(returnValue);
197+
Assert.False(result.Value.WasEmptyOrNull);
198+
}
199+
200+
[Fact]
201+
public void TryConvertValue_ForCustomParsableStruct_UsesParsableImplementation_ForBadValue()
202+
{
203+
var culture = CultureInfo.GetCultureInfo("en-US");
204+
205+
var nullableConverter = new NullableConverter<ParsableTestStruct>(new ParsableConverter<ParsableTestStruct>());
206+
var reader = new FormDataReader(default, culture, default)
207+
{
208+
ErrorHandler = (_, __, ___) => { }
209+
};
210+
211+
var returnValue = nullableConverter.TryConvertValue(ref reader, "bad value", out var result);
212+
213+
Assert.False(returnValue);
214+
}
215+
216+
private struct ParsableTestStruct : IParsable<ParsableTestStruct>
217+
{
218+
public bool WasEmptyOrNull { get; set; }
219+
220+
public static ParsableTestStruct Parse(string s, IFormatProvider provider) => throw new NotImplementedException();
221+
222+
public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out ParsableTestStruct result)
223+
{
224+
if (string.IsNullOrEmpty(s))
225+
{
226+
result = new ParsableTestStruct { WasEmptyOrNull = true };
227+
return true;
228+
}
229+
else if (s == "good value")
230+
{
231+
result = new ParsableTestStruct { WasEmptyOrNull = false };
232+
return true;
233+
}
234+
else
235+
{
236+
result = new();
237+
return false;
238+
}
239+
}
240+
}
150241
}

0 commit comments

Comments
 (0)