Skip to content

Commit ff827d2

Browse files
nvmkpkPraveen Potturu
and
Praveen Potturu
authored
[Blazor] Treating empty string in form post as null for nullable value types (#52499)
* Treating empty string in form post as null for nullable value types * Adding seven unit tests for the changes * Adding e2e test for the changes --------- Co-authored-by: Praveen Potturu <[email protected]>
1 parent 81b2894 commit ff827d2

File tree

4 files changed

+233
-6
lines changed

4 files changed

+233
-6
lines changed

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

+18-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ 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))
18+
{
19+
// Form post sends empty string for a form field that does not have a value,
20+
// in case of nullable value types, that should be treated as null and
21+
// should not be parsed for its underlying type
22+
result = null;
23+
return true;
24+
}
25+
1726
var converter = (ISingleValueConverter<T>)_nonNullableConverter;
1827

1928
if (converter.TryConvertValue(ref reader, value, out var converted))
@@ -30,17 +39,20 @@ public bool TryConvertValue(ref FormDataReader reader, string value, out T? resu
3039

3140
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
3241
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
33-
internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found)
42+
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found)
3443
{
35-
if (!(_nonNullableConverter.TryRead(ref context, type, options, out var innerResult, out found) && found))
44+
// Donot call non-nullable converter's TryRead method, it will fail to parse empty
45+
// string. Call the TryConvertValue method above (similar to ParsableConverter) so
46+
// that it can handle the empty string correctly
47+
found = reader.TryGetValue(out var value);
48+
if (!found)
3649
{
37-
result = null;
38-
return false;
50+
result = default;
51+
return true;
3952
}
4053
else
4154
{
42-
result = innerResult;
43-
return true;
55+
return TryConvertValue(ref reader, value!, out result!);
4456
}
4557
}
4658
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
6+
using Microsoft.Extensions.Primitives;
7+
8+
namespace Microsoft.AspNetCore.Components.Endpoints.Tests.FormMapping;
9+
10+
public class NullableConverterTests
11+
{
12+
[Fact]
13+
public void TryConvertValue_ForDateOnlyReturnsTrueWithNullForEmptyString()
14+
{
15+
var culture = CultureInfo.GetCultureInfo("en-US");
16+
17+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
18+
var reader = new FormDataReader(default, culture, default);
19+
20+
var returnValue = nullableConverter.TryConvertValue(ref reader, string.Empty, out var result);
21+
22+
Assert.True(returnValue);
23+
Assert.Null(result);
24+
}
25+
26+
[Fact]
27+
public void TryConvertValue_ForDateOnlyReturnsTrueWithDateForRealDateValue()
28+
{
29+
var date = new DateOnly(2023, 11, 30);
30+
var culture = CultureInfo.GetCultureInfo("en-US");
31+
32+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
33+
var reader = new FormDataReader(default, culture, default);
34+
35+
var returnValue = nullableConverter.TryConvertValue(ref reader, date.ToString(culture), out var result);
36+
37+
Assert.True(returnValue);
38+
Assert.Equal(date, result);
39+
}
40+
41+
[Fact]
42+
public void TryConvertValue_ForDateOnlyReturnsFalseWithNullForBadDateValue()
43+
{
44+
var culture = CultureInfo.GetCultureInfo("en-US");
45+
46+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
47+
var reader = new FormDataReader(default, culture, default)
48+
{
49+
ErrorHandler = (_, __, ___) => { }
50+
};
51+
52+
var returnValue = nullableConverter.TryConvertValue(ref reader, "bad date", out var result);
53+
54+
Assert.False(returnValue);
55+
Assert.Null(result);
56+
}
57+
58+
[Fact]
59+
public void TryRead_ForDateOnlyReturnsFalseWithNullForNoValue()
60+
{
61+
const string prefixName = "field";
62+
var culture = CultureInfo.GetCultureInfo("en-US");
63+
64+
var dictionary = new Dictionary<FormKey, StringValues>();
65+
var buffer = prefixName.ToCharArray().AsMemory();
66+
var reader = new FormDataReader(dictionary, culture, buffer);
67+
reader.PushPrefix(prefixName);
68+
69+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
70+
71+
var returnValue = nullableConverter.TryRead(ref reader, typeof(DateOnly?), default, out var result, out var found);
72+
73+
Assert.False(found);
74+
Assert.True(returnValue);
75+
Assert.Null(result);
76+
}
77+
78+
[Fact]
79+
public void TryRead_ForDateOnlyReturnsTrueWithNullForEmptyString()
80+
{
81+
const string prefixName = "field";
82+
var culture = CultureInfo.GetCultureInfo("en-US");
83+
84+
var dictionary = new Dictionary<FormKey, StringValues>()
85+
{
86+
{ new FormKey(prefixName.AsMemory()), (StringValues)string.Empty }
87+
};
88+
var buffer = prefixName.ToCharArray().AsMemory();
89+
var reader = new FormDataReader(dictionary, culture, buffer);
90+
reader.PushPrefix(prefixName);
91+
92+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
93+
94+
var returnValue = nullableConverter.TryRead(ref reader, typeof(DateOnly?), default, out var result, out var found);
95+
96+
Assert.True(found);
97+
Assert.True(returnValue);
98+
Assert.Null(result);
99+
}
100+
101+
[Fact]
102+
public void TryRead_ForDateOnlyReturnsTrueWithDateForRealDateValue()
103+
{
104+
const string prefixName = "field";
105+
var date = new DateOnly(2023, 11, 30);
106+
var culture = CultureInfo.GetCultureInfo("en-US");
107+
108+
var dictionary = new Dictionary<FormKey, StringValues>()
109+
{
110+
{ new FormKey(prefixName.AsMemory()), (StringValues)date.ToString(culture) }
111+
};
112+
var buffer = prefixName.ToCharArray().AsMemory();
113+
var reader = new FormDataReader(dictionary, culture, buffer);
114+
reader.PushPrefix(prefixName);
115+
116+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
117+
118+
var returnValue = nullableConverter.TryRead(ref reader, typeof(DateOnly?), default, out var result, out var found);
119+
120+
Assert.True(found);
121+
Assert.True(returnValue);
122+
Assert.Equal(date, result);
123+
}
124+
125+
[Fact]
126+
public void TryRead_ForDateOnlyReturnsFalseWithNullForBadDateValue()
127+
{
128+
const string prefixName = "field";
129+
var culture = CultureInfo.GetCultureInfo("en-US");
130+
131+
var dictionary = new Dictionary<FormKey, StringValues>()
132+
{
133+
{ new FormKey(prefixName.AsMemory()), (StringValues)"bad date" }
134+
};
135+
var buffer = prefixName.ToCharArray().AsMemory();
136+
var reader = new FormDataReader(dictionary, culture, buffer)
137+
{
138+
ErrorHandler = (_, __, ___) => { }
139+
};
140+
reader.PushPrefix(prefixName);
141+
142+
var nullableConverter = new NullableConverter<DateOnly>(new ParsableConverter<DateOnly>());
143+
144+
var returnValue = nullableConverter.TryRead(ref reader, typeof(DateOnly?), default, out var result, out var found);
145+
146+
Assert.True(found);
147+
Assert.False(returnValue);
148+
Assert.Null(result);
149+
}
150+
}

src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs

+23
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,29 @@ public void CanDispatchToFormDefinedInNonPageComponent(bool suppressEnhancedNavi
772772
DispatchToFormCore(dispatchToForm);
773773
}
774774

775+
[Theory]
776+
[InlineData(null)]
777+
[InlineData("")]
778+
[InlineData("01/01/2000")]
779+
public void FormWithNullableDateTime(string value)
780+
{
781+
var dispatchToForm = new DispatchToForm(this)
782+
{
783+
Url = "forms/with-nullable-datetime",
784+
FormCssSelector = "form[id=nullable]",
785+
ExpectedHandlerValue = "nullable-datetime-testform",
786+
InputFieldId = "Id",
787+
InputFieldCssSelector = "form[id=nullable] input[id=datetime]",
788+
InputFieldValue = value,
789+
AssertErrors = errors =>
790+
{
791+
Assert.Empty(errors);
792+
},
793+
};
794+
795+
DispatchToFormCore(dispatchToForm);
796+
}
797+
775798
[Fact]
776799
public void CanRenderAmbiguousForms()
777800
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@page "/forms/with-nullable-datetime"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
<h3>Edit Form With Nullable DateTime</h3>
4+
<EditForm Enhance Model="@this.Model" method="post" id="nullable" FormName="nullable-datetime-testform" OnValidSubmit="this.HandleSubmit">
5+
<ValidationSummary />
6+
<div>
7+
<label for="Model.NullableDateTime">
8+
Date:
9+
<input type="date" id="datetime" name="Model.NullableDateTime" @bind-value="this.Model.NullableDateTime" />
10+
</label>
11+
<ValidationMessage For="() => Model.NullableDateTime" />
12+
</div>
13+
<button id="send" type="submit">Submit</button>
14+
</EditForm>
15+
@code {
16+
[SupplyParameterFromForm]
17+
public FormObject Model
18+
{
19+
get;
20+
set;
21+
}
22+
23+
protected override void OnInitialized()
24+
{
25+
base.OnInitialized();
26+
27+
this.Model ??= new FormObject();
28+
}
29+
30+
private void HandleSubmit()
31+
{
32+
}
33+
34+
public class FormObject
35+
{
36+
public DateTime? NullableDateTime
37+
{
38+
get;
39+
set;
40+
}
41+
};
42+
}

0 commit comments

Comments
 (0)