Skip to content

Commit 27f93bf

Browse files
Copilotjaviercn
andcommitted
Identify the root cause: missing return in HttpContextFormValueMapper.Map
Co-authored-by: javiercn <[email protected]>
1 parent e78e8d4 commit 27f93bf

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

src/Components/Endpoints/test/Binding/FormDataMapperTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.AspNetCore.Components.Forms;
1414
using Microsoft.AspNetCore.Http;
1515
using Microsoft.Extensions.Primitives;
16+
using Microsoft.Extensions.Logging.Abstractions;
1617

1718
namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
1819

@@ -1464,6 +1465,44 @@ public void CanDeserialize_ComplexRecursiveTypes_ThrowsWhenMaxRecursionDepthExce
14641465
});
14651466
}
14661467

1468+
[Fact]
1469+
public void CanDeserialize_SimpleRecursiveModel_WithOnlyNameProperty()
1470+
{
1471+
// This reproduces the issue from GitHub #61341
1472+
// A model with a recursive property that has no form data provided
1473+
var data = new Dictionary<string, StringValues>()
1474+
{
1475+
["Name"] = "Test Name"
1476+
// Note: No data for Parent property
1477+
};
1478+
1479+
var reader = CreateFormDataReader(data, CultureInfo.InvariantCulture);
1480+
var options = new FormDataMapperOptions();
1481+
1482+
// Act
1483+
var result = FormDataMapper.Map<MyModel>(reader, options);
1484+
1485+
// Assert
1486+
Assert.NotNull(result);
1487+
Assert.Equal("Test Name", result.Name);
1488+
Assert.Null(result.Parent);
1489+
}
1490+
1491+
[Fact]
1492+
public void ComplexTypeConverterFactory_CanConvert_RecursiveType()
1493+
{
1494+
// Test if ComplexTypeConverterFactory can convert recursive types
1495+
// This tests the exact scenario from GitHub issue #61341
1496+
var options = new FormDataMapperOptions();
1497+
var factory = new ComplexTypeConverterFactory(options, new NullLoggerFactory());
1498+
1499+
// Act
1500+
var canConvert = factory.CanConvert(typeof(MyModel), options);
1501+
1502+
// Assert
1503+
Assert.True(canConvert, "ComplexTypeConverterFactory should be able to convert recursive types");
1504+
}
1505+
14671506
[Fact]
14681507
public void CanDeserialize_ComplexRecursiveCollectionTypes_RecursiveTree()
14691508
{
@@ -2449,6 +2488,12 @@ internal class RecursiveDictionaryTree
24492488

24502489
internal record ClassRecordType(string Key, int Value);
24512490

2491+
internal class MyModel
2492+
{
2493+
public string Name { get; set; }
2494+
public MyModel Parent { get; set; }
2495+
}
2496+
24522497
internal record struct StructRecordType(string Key, int Value);
24532498

24542499
internal class DataMemberAttributesType

src/Components/Endpoints/test/HttpContextFormValueMapperTest.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,26 @@ public void CanMap_MatchesOnScopeAndFormName(bool expectedResult, string incomin
4242
var canMap = mapper.CanMap(typeof(string), scopeName, formNameOrNull);
4343
Assert.Equal(expectedResult, canMap);
4444
}
45+
46+
[Fact]
47+
public void CanMap_SimpleRecursiveModel_ReturnsTrue()
48+
{
49+
// Test that CanMap works correctly for recursive types (GitHub issue #61341)
50+
var formData = new HttpContextFormDataProvider();
51+
formData.SetFormData("test-form", new Dictionary<string, StringValues>
52+
{
53+
["Name"] = "Test Name"
54+
}, new FormFileCollection());
55+
56+
var mapper = new HttpContextFormValueMapper(formData, Options.Create<RazorComponentsServiceOptions>(new()));
57+
58+
var canMap = mapper.CanMap(typeof(MyModel), "", null);
59+
Assert.True(canMap);
60+
}
61+
}
62+
63+
internal class MyModel
64+
{
65+
public string Name { get; set; }
66+
public MyModel Parent { get; set; }
4567
}

src/Components/Web/test/Forms/Mapping/SupplyParameterFromFormTest.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.Components.Forms.Mapping;
55
using Microsoft.AspNetCore.Components.Test.Helpers;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
78

89
namespace Microsoft.AspNetCore.Components.Forms.PostHandling;
910

@@ -58,6 +59,26 @@ public async Task FindCascadingParameters_HandlesSupplyParameterFromFormValues_W
5859
Assert.Equal(formMappingScope, supplier.ValueSupplier);
5960
}
6061

62+
[Fact]
63+
public async Task FindCascadingParameters_HandlesRecursiveModelTypes()
64+
{
65+
// This test reproduces GitHub issue #61341
66+
// Arrange
67+
var renderer = CreateRendererWithFormValueModelBinder();
68+
var formComponent = new RecursiveFormParametersComponent();
69+
70+
// Act
71+
var componentId = renderer.AssignRootComponentId(formComponent);
72+
await renderer.RenderRootComponentAsync(componentId);
73+
var formComponentState = renderer.GetComponentState(formComponent);
74+
75+
var result = CascadingParameterState.FindCascadingParameters(formComponentState, out _);
76+
77+
// Assert
78+
var supplier = Assert.Single(result);
79+
Assert.IsType<SupplyParameterFromFormValueProvider>(supplier.ValueSupplier);
80+
}
81+
6182
static TestRenderer CreateRendererWithFormValueModelBinder()
6283
{
6384
var services = new ServiceCollection();
@@ -78,6 +99,17 @@ class FormParametersComponentWithName : TestComponentBase
7899
[SupplyParameterFromForm(FormName = "handler-name")] public string FormParameter { get; set; }
79100
}
80101

102+
class RecursiveFormParametersComponent : TestComponentBase
103+
{
104+
[SupplyParameterFromForm] public MyModel FormParameter { get; set; }
105+
}
106+
107+
class MyModel
108+
{
109+
public string Name { get; set; }
110+
public MyModel Parent { get; set; }
111+
}
112+
81113
class TestFormModelValueBinder(string IncomingScopeQualifiedFormName = "") : IFormValueMapper
82114
{
83115
public void Map(FormValueMappingContext context) { }
@@ -95,6 +127,40 @@ public bool CanMap(Type valueType, string mappingScopeName, string formName)
95127
}
96128
}
97129

130+
class TestFormValueMapperWithRealBinding : IFormValueMapper
131+
{
132+
public void Map(FormValueMappingContext context)
133+
{
134+
// Create a minimal FormDataMapperOptions to test type resolution
135+
var options = new FormDataMapperOptions();
136+
try
137+
{
138+
var converter = options.ResolveConverter(context.ValueType);
139+
if (converter != null)
140+
{
141+
context.SetResult(Activator.CreateInstance(context.ValueType));
142+
}
143+
}
144+
catch
145+
{
146+
// If there's an exception resolving the converter, don't set result
147+
}
148+
}
149+
150+
public bool CanMap(Type valueType, string mappingScopeName, string formName)
151+
{
152+
try
153+
{
154+
var options = new FormDataMapperOptions();
155+
return options.CanConvert(valueType);
156+
}
157+
catch
158+
{
159+
return false;
160+
}
161+
}
162+
}
163+
98164
class TestComponentBase : IComponent
99165
{
100166
public void Attach(RenderHandle renderHandle)

0 commit comments

Comments
 (0)