From 06d6c3042107cd7786855ffacf0cede7e748e352 Mon Sep 17 00:00:00 2001 From: Alex <57764940+axnetg@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:39:55 +0100 Subject: [PATCH 1/2] fix: Do not throw ArgumentException when request query has an empty key --- .../Query/ODataQueryOptions.cs | 2 +- .../Query/Query/ODataQueryOptionTests.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs index 67ee155be..89b582ee8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs @@ -886,7 +886,7 @@ private IDictionary GetODataQueryParameters() } else { - if (IsSupportedQueryOption(key)) + if (!string.IsNullOrEmpty(key) && IsSupportedQueryOption(key)) { // Normalized the supported system query key by adding the $-prefix if needed. result.Add( diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs index b4eeefb09..c81ce7cb5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs @@ -1280,6 +1280,27 @@ public async Task DuplicateUnsupportedQueryParametersIgnoredWithNoException() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + [Theory] + [InlineData("$top=10&$skip=0&%20")] + [InlineData("$top=10&%20=foo&$skip=0")] + [InlineData("$top=10&$skip=0&")] + [InlineData("$top=10&&$skip=0")] + [InlineData("%20")] + public void ODataQueryOptions_CtorDoesNotThrowOnEmptyQueryStringKeys(string oDataQuery) + { + // Arrange + IEdmModel model = GetEdmModelWithoutKey(); + + string url = "http://server/service/Customers?" + oDataQuery; + HttpRequest request = RequestFactory.Create(HttpMethods.Get, url, opt => opt.EnableNoDollarQueryOptions = true); + + // Act + var exception = Record.Exception(() => new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request)); + + // Assert + Assert.Null(exception); + } + private static HttpClient CreateClient() { var controllers = new[] { typeof(EntityModelsController), typeof(ProductsController) }; From 47e04156ce973117d8aa4141905f7f30e47ee54e Mon Sep 17 00:00:00 2001 From: Alex <57764940+axnetg@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:04:40 +0100 Subject: [PATCH 2/2] test: E2E test coverage --- .../IgnoreEmptyParamsTest.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/IgnoreEmptyParamsTest.cs diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/IgnoreEmptyParamsTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/IgnoreEmptyParamsTest.cs new file mode 100644 index 000000000..c12deedc8 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/IgnoreEmptyParamsTest.cs @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//-- + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class IgnoreEmptyParamsTest : WebApiTestBase +{ + public IgnoreEmptyParamsTest(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); + + IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); + + services.AddControllers().AddOData(opt => + { + opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Select().Expand().SetMaxTop(null); + opt.EnableNoDollarQueryOptions = true; + }); + } + + public static TheoryDataSet IgnoreEmptyParamsCases + { + get + { + return new TheoryDataSet() + { + { "Get", "Customers?$top=10&$skip=0", "Customers?$top=10&$skip=0&%20" }, + { "Get", "Customers?$select=Name,Id", "Customers?%20=foo&$select=Name,Id" }, + { "Get", "Customers?$orderby=Name", "Customers?%20=foo$orderby=Name" }, + { "Get", "Customers?$filter=Name eq 'test'", "Customers?$filter=Name eq 'test'&" }, + { "Get", "Customers?$count=true", "Customers?%20=%20&$count=true" }, + + { "Get", "Customers?top=10&skip=0", "Customers?top=10&skip=0&%20" }, + { "Get", "Customers?select=Name,Id", "Customers?&%20=foo&select=Name,Id" }, + { "Get", "Customers?orderby=Name", "Customers?%20=foo&orderby=Name" }, + { "Get", "Customers?filter=Name eq 'test'", "Customers?filter=Name eq 'test'&" }, + { "Get", "Customers?count=true", "Customers?%20=%20&count=true" }, + + { "Get", "Customers", "Customers?%20" }, + { "Get", "Customers(1)", "Customers(1)?=foo" }, + }; + } + } + + [Theory] + [MemberData(nameof(IgnoreEmptyParamsCases))] + public async Task ParserIgnoresEmptyParamsTest(string method, string baselinePath, string emptyParamsPath) + { + // Baseline scenario: query params are all correct + HttpClient client = CreateClient(); + + var baselineUri = $"odata/{baselinePath}"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), baselineUri); + HttpResponseMessage response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string baselineResponse = await response.Content.ReadAsStringAsync(); + + // Test scenario: some params are injected in the query string, such as empty spaces + var emptyParamsUri = $"odata/{emptyParamsPath}"; + request = new HttpRequestMessage(new HttpMethod(method), emptyParamsUri); + response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string emptyParamsResponse = await response.Content.ReadAsStringAsync(); + + // Expected behavior: empty params are ignored and responses from both scenarios are equivalent + Assert.Equal(baselineResponse, emptyParamsResponse); + } +}