From 0a09d064722b1389ca619a2d6e39c22e59a62c13 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 11 Jun 2026 17:16:16 -0700 Subject: [PATCH 01/12] Fix bug --- src/Core/Parsers/RequestParser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Parsers/RequestParser.cs b/src/Core/Parsers/RequestParser.cs index 081018e820..d802abaf78 100644 --- a/src/Core/Parsers/RequestParser.cs +++ b/src/Core/Parsers/RequestParser.cs @@ -321,6 +321,8 @@ private static bool IsNull(string value) return null; } + parameterName = parameterName.Replace("$", "%24"); + // Split on '&' which are parameter separators in properly URL-encoded query strings. // Any '&' characters within parameter values will be encoded as %26. foreach (string param in queryString.TrimStart('?').Split('&')) From 024e0b08656ca1b53a2ceb2bf1ea01dd9bd1ce31 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 12 Jun 2026 10:20:36 -0700 Subject: [PATCH 02/12] Fix bug --- src/Core/Parsers/RequestParser.cs | 2 -- src/Core/Services/RestService.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Core/Parsers/RequestParser.cs b/src/Core/Parsers/RequestParser.cs index d802abaf78..081018e820 100644 --- a/src/Core/Parsers/RequestParser.cs +++ b/src/Core/Parsers/RequestParser.cs @@ -321,8 +321,6 @@ private static bool IsNull(string value) return null; } - parameterName = parameterName.Replace("$", "%24"); - // Split on '&' which are parameter separators in properly URL-encoded query strings. // Any '&' characters within parameter values will be encoded as %26. foreach (string param in queryString.TrimStart('?').Split('&')) diff --git a/src/Core/Services/RestService.cs b/src/Core/Services/RestService.cs index 5014d942bb..0db66ccbcc 100644 --- a/src/Core/Services/RestService.cs +++ b/src/Core/Services/RestService.cs @@ -71,7 +71,7 @@ RequestValidator requestValidator DatabaseObject dbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; QueryString? query = GetHttpContext().Request.QueryString; - string queryString = query is null ? string.Empty : GetHttpContext().Request.QueryString.ToString(); + string queryString = query is null ? string.Empty : GetHttpContext().Request.QueryString.ToString().Replace("%24", "$"); //Add replacement in order to ensure '$' sign is present as expected // Read the request body early so it can be used for downstream processing. string requestBody = string.Empty; From a5e72f45b8a4b748c992ad08057e4a47b4557a0c Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 12 Jun 2026 11:39:17 -0700 Subject: [PATCH 03/12] Add testing --- .../Configuration/ConfigurationTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 203de98aef..7499a24939 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4224,6 +4224,42 @@ public async Task OpenApi_InteractiveSwaggerUI( } } + /// + /// End to end test that validates that REST requests with OData query + /// options $filter and $orderby succeed to ensure no regression can occur. + /// + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public async Task TestForRestRequestsWithFilterAndOrderbyParameters() + { + // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the + // configuration file (instead of using CLI). + string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, BOOK_ENTITY_JSON); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + connectionString: GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); + string configFileName = "custom-config.json"; + File.WriteAllText(configFileName, deserializedConfig.ToJson()); + string[] args = new[] + { + $"--ConfigFileName={configFileName}" + }; + + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + // Act + RuntimeConfigProvider configProvider = server.Services.GetService(); + using HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book?$orderby=id desc&$filter=publisher_id eq 1234"); + using HttpResponseMessage restResponse = await client.SendAsync(restRequest); + + // Assert - Verify REST response + Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode, "REST request to auto-generated entity should succeed"); + } + } + /// /// Test different loglevel values that are avaliable by deserializing RuntimeConfig with specified LogLevel /// and checks if value exists properly inside the deserialized RuntimeConfig. From f83f1220b5d80daace1403496926e8a6f2879bd2 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 12 Jun 2026 12:02:28 -0700 Subject: [PATCH 04/12] Address comments by copilot --- src/Core/Services/RestService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/Services/RestService.cs b/src/Core/Services/RestService.cs index 0db66ccbcc..bb3888e7d9 100644 --- a/src/Core/Services/RestService.cs +++ b/src/Core/Services/RestService.cs @@ -71,7 +71,8 @@ RequestValidator requestValidator DatabaseObject dbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; QueryString? query = GetHttpContext().Request.QueryString; - string queryString = query is null ? string.Empty : GetHttpContext().Request.QueryString.ToString().Replace("%24", "$"); //Add replacement in order to ensure '$' sign is present as expected + string queryString = query is null ? string.Empty : GetHttpContext().Request.QueryString.ToString() + .Replace("?%24", "?$").Replace("&%24", "&$"); //Add replacement in order to ensure '$' sign is present as expected only for the case where it is after a '?' or '&' signs. // Read the request body early so it can be used for downstream processing. string requestBody = string.Empty; From d2eee0d91e7f3ecc4442e89b4062b4b232511c95 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 12 Jun 2026 14:20:40 -0700 Subject: [PATCH 05/12] Address comments based on copilot --- src/Service.Tests/Configuration/ConfigurationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 7499a24939..8c7f64f4e0 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4251,7 +4251,6 @@ public async Task TestForRestRequestsWithFilterAndOrderbyParameters() using (HttpClient client = server.CreateClient()) { // Act - RuntimeConfigProvider configProvider = server.Services.GetService(); using HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book?$orderby=id desc&$filter=publisher_id eq 1234"); using HttpResponseMessage restResponse = await client.SendAsync(restRequest); From b3e59d5b69aea5eb9d74a87d3dad58559c43d24e Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 11:26:25 -0700 Subject: [PATCH 06/12] Changes by copilot --- src/Core/Parsers/RequestParser.cs | 4 ++++ src/Core/Services/RestService.cs | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Core/Parsers/RequestParser.cs b/src/Core/Parsers/RequestParser.cs index 081018e820..27684bd773 100644 --- a/src/Core/Parsers/RequestParser.cs +++ b/src/Core/Parsers/RequestParser.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using System.Web; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Service.Exceptions; @@ -321,6 +322,9 @@ private static bool IsNull(string value) return null; } + // Encode the parameterName to ensure it matches the encoding in the query string. + parameterName = HttpUtility.UrlEncode(parameterName); + // Split on '&' which are parameter separators in properly URL-encoded query strings. // Any '&' characters within parameter values will be encoded as %26. foreach (string param in queryString.TrimStart('?').Split('&')) diff --git a/src/Core/Services/RestService.cs b/src/Core/Services/RestService.cs index bb3888e7d9..5014d942bb 100644 --- a/src/Core/Services/RestService.cs +++ b/src/Core/Services/RestService.cs @@ -71,8 +71,7 @@ RequestValidator requestValidator DatabaseObject dbObject = sqlMetadataProvider.EntityToDatabaseObject[entityName]; QueryString? query = GetHttpContext().Request.QueryString; - string queryString = query is null ? string.Empty : GetHttpContext().Request.QueryString.ToString() - .Replace("?%24", "?$").Replace("&%24", "&$"); //Add replacement in order to ensure '$' sign is present as expected only for the case where it is after a '?' or '&' signs. + string queryString = query is null ? string.Empty : GetHttpContext().Request.QueryString.ToString(); // Read the request body early so it can be used for downstream processing. string requestBody = string.Empty; From ea83edc6978b6f3a9f0c6a30ff7ffa160b6ef5d6 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 12:08:59 -0700 Subject: [PATCH 07/12] Add missing test --- src/Service.Tests/Configuration/ConfigurationTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 8c7f64f4e0..16c016d1ec 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4228,9 +4228,11 @@ public async Task OpenApi_InteractiveSwaggerUI( /// End to end test that validates that REST requests with OData query /// options $filter and $orderby succeed to ensure no regression can occur. /// - [TestMethod] + [DataTestMethod] + [DataRow("/api/Book?$orderby=id desc&$filter=publisher_id eq 1234", DisplayName = "Regular REST uri")] + [DataRow("/api/Book?%24orderby=id%20desc&%24filter=publisher_id%20eq%201234", DisplayName = "Regular REST uri")] [TestCategory(TestCategory.MSSQL)] - public async Task TestForRestRequestsWithFilterAndOrderbyParameters() + public async Task TestForRestRequestsWithFilterAndOrderbyParameters(string restUri) { // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the // configuration file (instead of using CLI). From c6388be544abbbb727049aa98029db5f33233dfb Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 12:19:08 -0700 Subject: [PATCH 08/12] Address comment --- src/Service.Tests/Configuration/ConfigurationTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 16c016d1ec..2c01426ce4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4258,6 +4258,10 @@ public async Task TestForRestRequestsWithFilterAndOrderbyParameters(string restU // Assert - Verify REST response Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode, "REST request to auto-generated entity should succeed"); + + string restResponseBody = await restResponse.Content.ReadAsStringAsync(); + Assert.IsTrue(!string.IsNullOrEmpty(restResponseBody), "REST response should contain data"); + Assert.IsTrue(restResponseBody.Contains("1234")); } } From 94e08c71b49f2300a80adc48c5394e45ad3e07e6 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 12:21:42 -0700 Subject: [PATCH 09/12] Address comment --- src/Service.Tests/Configuration/ConfigurationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 2c01426ce4..2717520d25 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4261,7 +4261,7 @@ public async Task TestForRestRequestsWithFilterAndOrderbyParameters(string restU string restResponseBody = await restResponse.Content.ReadAsStringAsync(); Assert.IsTrue(!string.IsNullOrEmpty(restResponseBody), "REST response should contain data"); - Assert.IsTrue(restResponseBody.Contains("1234")); + Assert.IsTrue(restResponseBody.Contains("\"publisher_id\": 1234")); } } From b58ecab1e75e114b4a76ac9c4115b0a5132d6043 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 15:30:15 -0700 Subject: [PATCH 10/12] Changes by copilot --- src/Core/Parsers/RequestParser.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Core/Parsers/RequestParser.cs b/src/Core/Parsers/RequestParser.cs index 27684bd773..4c919b6097 100644 --- a/src/Core/Parsers/RequestParser.cs +++ b/src/Core/Parsers/RequestParser.cs @@ -322,8 +322,11 @@ private static bool IsNull(string value) return null; } - // Encode the parameterName to ensure it matches the encoding in the query string. - parameterName = HttpUtility.UrlEncode(parameterName); + // Encode the parameterName to ensure it matches the encoding in the query string if the $ sign is URL encoded. + if (!queryString.Contains(parameterName)) + { + parameterName = HttpUtility.UrlEncode(parameterName); + } // Split on '&' which are parameter separators in properly URL-encoded query strings. // Any '&' characters within parameter values will be encoded as %26. From a4d7c74e686f1f26253e812344267564589b3078 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 16:59:43 -0700 Subject: [PATCH 11/12] Fix tests --- src/Core/Parsers/RequestParser.cs | 2 +- src/Service.Tests/Configuration/ConfigurationTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Core/Parsers/RequestParser.cs b/src/Core/Parsers/RequestParser.cs index 4c919b6097..223cb3a38f 100644 --- a/src/Core/Parsers/RequestParser.cs +++ b/src/Core/Parsers/RequestParser.cs @@ -323,7 +323,7 @@ private static bool IsNull(string value) } // Encode the parameterName to ensure it matches the encoding in the query string if the $ sign is URL encoded. - if (!queryString.Contains(parameterName)) + if (!queryString.Contains(parameterName, StringComparison.OrdinalIgnoreCase)) { parameterName = HttpUtility.UrlEncode(parameterName); } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 2717520d25..bd0d320529 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4229,8 +4229,8 @@ public async Task OpenApi_InteractiveSwaggerUI( /// options $filter and $orderby succeed to ensure no regression can occur. /// [DataTestMethod] - [DataRow("/api/Book?$orderby=id desc&$filter=publisher_id eq 1234", DisplayName = "Regular REST uri")] - [DataRow("/api/Book?%24orderby=id%20desc&%24filter=publisher_id%20eq%201234", DisplayName = "Regular REST uri")] + [DataRow("/api/Book?$orderby=id desc&$filter=publisher_id eq 1234", DisplayName = "REST URL without encoded characters")] + [DataRow("/api/Book?%24orderby=id%20desc&%24filter=publisher_id%20eq%201234", DisplayName = "REST URL with encoded characters")] [TestCategory(TestCategory.MSSQL)] public async Task TestForRestRequestsWithFilterAndOrderbyParameters(string restUri) { @@ -4261,7 +4261,7 @@ public async Task TestForRestRequestsWithFilterAndOrderbyParameters(string restU string restResponseBody = await restResponse.Content.ReadAsStringAsync(); Assert.IsTrue(!string.IsNullOrEmpty(restResponseBody), "REST response should contain data"); - Assert.IsTrue(restResponseBody.Contains("\"publisher_id\": 1234")); + Assert.IsTrue(restResponseBody.Contains("\"publisher_id\":1234")); } } From f22f40843265c64165769a918a535ced7a6e2f9f Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 16 Jun 2026 20:52:33 -0700 Subject: [PATCH 12/12] Fix test --- src/Service.Tests/Configuration/ConfigurationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index bd0d320529..71ef6ed6cf 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4253,7 +4253,7 @@ public async Task TestForRestRequestsWithFilterAndOrderbyParameters(string restU using (HttpClient client = server.CreateClient()) { // Act - using HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book?$orderby=id desc&$filter=publisher_id eq 1234"); + using HttpRequestMessage restRequest = new(HttpMethod.Get, restUri); using HttpResponseMessage restResponse = await client.SendAsync(restRequest); // Assert - Verify REST response