Skip to content

Commit d9a1cf0

Browse files
authored
Add tests and fix exception handling in form binding (#49983)
1 parent bfeb3bf commit d9a1cf0

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
lines changed

src/Components/Endpoints/src/FormMapping/FormDataReader.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ public void AddMappingError(Exception exception, string? attemptedValue)
7373
{
7474
ArgumentNullException.ThrowIfNull(exception);
7575

76+
// Avoid re-wrapping the exception if it is already a FormDataMappingException
77+
// and we don't have an ErrorHandler configured.
78+
if (exception is FormDataMappingException && ErrorHandler == null)
79+
{
80+
throw exception;
81+
}
82+
7683
var errorMessage = FormattableStringFactory.Create(exception.Message);
7784
AddMappingError(errorMessage, attemptedValue);
7885
}
@@ -183,7 +190,7 @@ internal void PopPrefix(ReadOnlySpan<char> key)
183190
_currentDepth--;
184191
Debug.Assert(_currentDepth >= 0);
185192
var keyLength = key.Length;
186-
// If keyLength is bigger than the current scope keyLength typically means there is a
193+
// If keyLength is bigger than the current scope keyLength typically means there is a
187194
// bug where some part of the code has not popped the scope appropriately.
188195
Debug.Assert(_currentPrefixBuffer.Length >= keyLength);
189196
if (_currentPrefixBuffer.Length == keyLength || _currentPrefixBuffer.Span[^(keyLength + 1)] != '.')

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,10 @@ private static Expression BindComplexParameterFromFormItem(
20392039
formDict,
20402040
Expression.Constant(CultureInfo.InvariantCulture),
20412041
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(formDataMapperOptions.MaxKeyBufferSize))));
2042+
// name_reader.MaxRecursionDepth = formDataMapperOptions.MaxRecursionDepth;
2043+
var setMaxRecursionDepthExpr = Expression.Assign(
2044+
Expression.Property(formReader, nameof(FormDataReader.MaxRecursionDepth)),
2045+
Expression.Constant(formDataMapperOptions.MaxRecursionDepth));
20422046
// FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
20432047
var invokeMapMethodExpr = Expression.Call(
20442048
FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
@@ -2063,6 +2067,7 @@ private static Expression BindComplexParameterFromFormItem(
20632067
Expression.Block(
20642068
processFormExpr,
20652069
initializeReaderExpr,
2070+
setMaxRecursionDepthExpr,
20662071
Expression.Assign(formArgument, invokeMapMethodExpr)),
20672072
conditionalReturnBufferExpr),
20682073
formArgument

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.FormMapping.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,122 @@ static void TestAction([FromForm] Dictionary<string, string> args) { }
167167

168168
// Assert - 2
169169
Assert.Equal("Specified argument was out of the range of valid values.", anotherException.Message);
170+
}
171+
172+
[Fact]
173+
public async Task SupportsFormMappingWithRecordTypes()
174+
{
175+
TodoRecord capturedTodo = default;
176+
void TestAction([FromForm] TodoRecord args) { capturedTodo = args; };
177+
var httpContext = CreateHttpContext();
178+
httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
179+
{
180+
{
181+
"id", "1"
182+
},
183+
{
184+
"name", "Write tests"
185+
},
186+
{
187+
"isCompleted", "false"
188+
}
189+
});
190+
191+
var factoryResult = RequestDelegateFactory.Create(TestAction);
192+
var requestDelegate = factoryResult.RequestDelegate;
193+
194+
await requestDelegate(httpContext);
195+
196+
Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
197+
Assert.Equal(1, capturedTodo.Id);
198+
Assert.Equal("Write tests", capturedTodo.Name);
199+
Assert.False(capturedTodo.IsCompleted);
200+
}
201+
202+
[Fact]
203+
public async Task SupportsRecursiveProperties()
204+
{
205+
Employee capturedEmployee = default;
206+
void TestAction([FromForm] Employee args) { capturedEmployee = args; };
207+
var httpContext = CreateHttpContext();
208+
httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
209+
{
210+
{
211+
"Name", "A"
212+
},
213+
{
214+
"Manager.Name", "B"
215+
},
216+
{
217+
"Manager.Manager.Name", "C"
218+
},
219+
{
220+
"Manager.Manager.Manager.Name", "D"
221+
}
222+
});
223+
224+
var factoryResult = RequestDelegateFactory.Create(TestAction);
225+
var requestDelegate = factoryResult.RequestDelegate;
226+
227+
await requestDelegate(httpContext);
228+
229+
Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
230+
}
170231

232+
[Fact]
233+
public async Task SupportsRecursivePropertiesWithRecursionLimit()
234+
{
235+
Employee capturedEmployee = default;
236+
var options = new RequestDelegateFactoryOptions
237+
{
238+
EndpointBuilder = CreateEndpointBuilder(new List<object>()
239+
{
240+
new FormMappingOptionsMetadata(maxRecursionDepth: 3)
241+
}),
242+
};
243+
var metadataResult = new RequestDelegateMetadataResult { EndpointMetadata = new List<object>() };
244+
void TestAction([FromForm] Employee args) { capturedEmployee = args; };
245+
var httpContext = CreateHttpContext();
246+
httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
247+
{
248+
{
249+
"Name", "A"
250+
},
251+
{
252+
"Manager.Name", "B"
253+
},
254+
{
255+
"Manager.Manager.Name", "C"
256+
},
257+
{
258+
"Manager.Manager.Manager.Name", "D"
259+
},
260+
{
261+
"Manager.Manager.Manager.Manager.Name", "E"
262+
},
263+
{
264+
"Manager.Manager.Manager.Manager.Manager.Name", "F"
265+
},
266+
{
267+
"Manager.Manager.Manager.Manager.Manager.Manager.Name", "G"
268+
}
269+
});
270+
271+
var factoryResult = RequestDelegateFactory.Create(TestAction, options, metadataResult);
272+
var requestDelegate = factoryResult.RequestDelegate;
273+
274+
var exception = await Assert.ThrowsAsync<FormDataMappingException>(async () => await requestDelegate(httpContext));
275+
276+
Assert.Equal("Manager.Manager.Manager", exception.Error.Key);
277+
Assert.Equal("The maximum recursion depth of '3' was exceeded for 'Manager.Manager.Manager.Name'.", exception.Error.Message.ToString(CultureInfo.InvariantCulture));
278+
279+
}
280+
281+
private record TodoRecord(int Id, string Name, bool IsCompleted);
282+
283+
private class Employee
284+
{
285+
public string Name { get; set; }
286+
public Employee Manager { get; set; }
171287
}
172288
}

0 commit comments

Comments
 (0)