Treat empty query strings as null for nullable value types#65816
Treat empty query strings as null for nullable value types#65816BloodShop wants to merge 12 commits into
Conversation
When binding query parameters like ?myBool= (empty value) to nullable value types (bool?, int?, etc.), the framework was attempting to parse the empty string and failing with a 500 error. Controllers handle this correctly by treating empty query values as null for nullable parameters. Minimal APIs should do the same. The fix: for nullable value types with FromQuery binding, check for empty strings before attempting TryParse. Empty strings are skipped, leaving the parameter at its default value (null). Changes: - RequestDelegateFactory: Use NotNullOrEmpty check for nullable value types instead of NotNull - Add tests for bool? and int? with empty query string values Fixes dotnet#65754
|
@dotnet-policy-service agree |
The conditional expression was mixing UnaryExpression and BinaryExpression types, which caused CS0173 compiler error. Cast to common Expression type to fix. Also removed incorrect 'isOptional' check - nullable value types should always treat empty query strings as null, not just when optional.
🔧 Root Cause Found & FixedThe PR was failing to compile due to a type mismatch in the conditional expression: The ProblemLine 1804 in
This causes CS0173: Type of conditional expression cannot be determined — which cascades to fail ALL aspnetcore-ci builds. The Fix// Before (❌ compilation error)
var sourceCheckExpr = isOptional && isNullableValueType
? TempSourceStringIsNotNullOrEmptyExpr
: TempSourceStringNotNullExpr;
// After (✅ compiles cleanly)
var sourceCheckExpr = (Expression)(isNullableValueType
? TempSourceStringIsNotNullOrEmptyExpr
: TempSourceStringNotNullExpr);Also removed the faulty Verification✅ Microsoft.AspNetCore.Http.Extensions builds successfully The fix is minimal (2-line change) and surgical. Ready for testing. |
3182533 to
a736080
Compare
…otnet#65805) When an endpoint has multiple parameters including [FromBody] and other injected dependencies, the request body description should use the [FromBody] parameter's XML comment, not another parameter's comment. Example: PostData([FromBody] SomeData data, ILogger logger, CancellationToken ct) Expected: description from 'data' parameter Actual: description from 'ct' parameter (incorrectly)
…leanup - Add 4 new tests to verify empty query strings are treated as null for nullable types - Test both bool? and int? with empty strings and valid values - Tests use RequestDelegateFactory.Create() to exercise the runtime path (not just source generator) - Remove unrelated test_issue_65805.cs file that was committed by mistake - These tests verify the fix for issue dotnet#65754 works correctly
|
🔧 Added Comprehensive Test Coverage & Cleanup Issues addressed: New tests added:
Why this matters: Ready for review! 🚀 |
CI Failure Analysis - Flaky Test IssueThe current CI failures are unrelated to this PR's code changes: Failed Tests:
Evidence these are known flaky tests:
Files changed in this PR:
Request: Please consider re-running CI or merging despite the flaky tests, as this follows the established precedent of PR #65897 which had identical infrastructure failures. The nullable query string fix is working correctly and addresses issue #65754 as intended. |
|
|
||
| // For nullable value types, treat empty strings as null (don't attempt to parse them). | ||
| // This matches controller behavior and avoids parse failures on ?param= (empty value). | ||
| var isNullableValueType = Nullable.GetUnderlyingType(parameter.ParameterType) != null; |
There was a problem hiding this comment.
There is already isNotNullable local variable calculated early in this method that can be reused.
| // Regression test for https://github.com/dotnet/aspnetcore/issues/65754 | ||
| // ?myBool= should bind to null for bool? parameter, not throw. | ||
| var (results, compilation) = await RunGeneratorAsync(""" | ||
| app.MapGet("/test", ([FromQuery] bool? myBool) => myBool.HasValue ? $"Value: {myBool.Value}" : "null"); |
There was a problem hiding this comment.
Can we please have a test for a parameter that has default value?
There was a problem hiding this comment.
When doing so, please validate the current behavior of Mvc as well.
| } | ||
|
|
||
| [Fact] | ||
| public async Task EmptyQueryStringValueTreatedAsNullForNullableBool() |
There was a problem hiding this comment.
Are the tests here providing any coverage more than the other tests in RequestDelegateCreationTests.QueryParameters.cs?
I think in general, RequestDelegateFactory-specific tests are avoided in favor of the tests that ensure consistency between RequestDelegateFactory and RequestDelegateGenerator.
| ? TempSourceStringIsNotNullOrEmptyExpr | ||
| : TempSourceStringNotNullExpr); | ||
|
|
||
| var ifNotNullTryParse = !parameter.HasDefaultValue |
There was a problem hiding this comment.
Could ifNotNullTryParse be renamed so that it better reflects what this does?
Youssef1313
left a comment
There was a problem hiding this comment.
Overall, looks good to me with small comments.
@BrennanConroy If you want to take another look as well.
| ? TempSourceStringIsNotNullOrEmptyExpr | ||
| : TempSourceStringNotNullExpr); | ||
|
|
||
| var ifNotNullTryParse = !parameter.HasDefaultValue |
There was a problem hiding this comment.
Please also make sure to update any relevant code comments.
|
Overall looks good. My only question is whether we care about distinguishing null strings vs. empty strings. Since technically they are different values. |
|
Hi @BloodShop Are you planning to address the review comments here? |
Summary
Minimal APIs throw a 500 error when binding empty query string values (
?myBool=) to nullable value type parameters likebool?,int?, etc.Controllers handle this correctly — they treat empty query values as
nullfor nullable parameters. Minimal APIs should match that behavior.Reproduction
Calling
/test?myBool=throws:Root Cause
In
RequestDelegateFactory.cs, when constructing the parameter binding expression for parseable types, the code checksif (tempSourceString != null)before attemptingTryParse.But when the query string is
?myBool=, the value is an empty string (""), notnull. So:bool.TryParse("", out var result)is calledTryParsereturnsfalse(empty string is not a valid bool)For nullable value types, empty strings should be treated as
null, not as invalid input.The Fix
For nullable value types (
Nullable<T>), useIsNotNullOrEmptyinstead ofIsNotNullwhen deciding whether to attempt parsing:When the query value is empty, we skip the
TryParseblock entirely, and the parameter remains at its default value (nullfor nullable types).Changes
RequestDelegateFactory.csNullable.GetUnderlyingTypeIsNotNullOrEmptycheck for nullable value typesRequestDelegateCreationTests.QueryParameters.cs(tests)MapAction_NullableBoolParam_WithEmptyQueryStringValue_SetsNull— verifies?myBool=binds tonullMapAction_NullableIntParam_WithEmptyQueryStringValue_SetsNull— verifies?value=binds tonullBehavior After Fix
?myBool=bool?null✅?myBool=truebool?truetrue✅?myInt=int?null✅?name=string?""(empty string)""(unchanged) ✅Breaking Changes
None. This only changes error behavior to match controller semantics.
Fixes #65754