diff --git a/src/Core/Resolvers/SqlPaginationUtil.cs b/src/Core/Resolvers/SqlPaginationUtil.cs
index 852138ef09..92e3b26e8f 100644
--- a/src/Core/Resolvers/SqlPaginationUtil.cs
+++ b/src/Core/Resolvers/SqlPaginationUtil.cs
@@ -644,13 +644,13 @@ public static string BuildQueryStringWithAfterToken(NameValueCollection? querySt
}
///
- /// Gets a consolidated next link for pagination in JSON format.
+ /// Builds the next-page URI used for cursor-based pagination.
///
- /// The base Pagination Uri
- /// The query string with after value
- /// True, if the next link should be relative
- ///
- public static JsonElement GetConsolidatedNextLinkForPagination(string baseUri, string queryString, bool isNextLinkRelative = false)
+ /// The base pagination URI.
+ /// The query string with the $after value already merged in.
+ /// True to return only the path + query (no host); false for an absolute URL.
+ /// The next-page URI as a string.
+ public static string BuildNextLinkUri(string baseUri, string queryString, bool isNextLinkRelative = false)
{
UriBuilder uriBuilder = new(baseUri)
{
@@ -659,17 +659,9 @@ public static JsonElement GetConsolidatedNextLinkForPagination(string baseUri, s
};
// Construct final link- absolute or relative
- string nextLinkValue = isNextLinkRelative
+ return isNextLinkRelative
? uriBuilder.Uri.PathAndQuery // returns just "/api/?$after...", no host
: uriBuilder.Uri.AbsoluteUri; // returns full URL
-
- // Return serialized JSON object
- string jsonString = JsonSerializer.Serialize(new[]
- {
- new { nextLink = nextLinkValue }
- });
-
- return JsonSerializer.Deserialize(jsonString);
}
///
diff --git a/src/Core/Resolvers/SqlResponseHelpers.cs b/src/Core/Resolvers/SqlResponseHelpers.cs
index 3736054d7f..fa5a5ee497 100644
--- a/src/Core/Resolvers/SqlResponseHelpers.cs
+++ b/src/Core/Resolvers/SqlResponseHelpers.cs
@@ -22,11 +22,11 @@ public class SqlResponseHelpers
{
///
- /// Format the results from a Find operation. Check if there is a requirement
- /// for a nextLink/after, and if so, add this value to the array of JsonElements to
- /// be used as part of the response.
+ /// Format the results from a Find operation. If a nextLink/after token is required for
+ /// pagination, the envelope is built directly via an anonymous response object so that
+ /// pagination metadata is carried out-of-band rather than encoded into the row collection.
///
- /// The JsonDocument from the query.
+ /// The JsonElement from the query (object for single-row, array for collections).
/// The RequestContext.
/// The metadataprovider.
/// Runtimeconfig object
@@ -41,7 +41,6 @@ public static OkObjectResult FormatFindResult(
HttpContext httpContext,
bool? isMcpRequest = null)
{
-
// When there are no rows returned from the database, the jsonElement will be an empty array.
// In that case, the response is returned as is.
if (findOperationResponse.ValueKind is JsonValueKind.Array && findOperationResponse.GetArrayLength() == 0)
@@ -49,41 +48,42 @@ public static OkObjectResult FormatFindResult(
return OkResponse(findOperationResponse);
}
- HashSet extraFieldsInResponse = (findOperationResponse.ValueKind is not JsonValueKind.Array)
- ? DetermineExtraFieldsInResponse(findOperationResponse, context.FieldsToBeReturned)
- : DetermineExtraFieldsInResponse(findOperationResponse.EnumerateArray().First(), context.FieldsToBeReturned);
+ bool isCollection = findOperationResponse.ValueKind is JsonValueKind.Array;
+
+ // Compute additional fields that were fetched for cursor/$orderby computation but
+ // are not part of $select and so should be stripped from the response payload.
+ JsonElement firstRowProbe = isCollection ? findOperationResponse.EnumerateArray().First() : findOperationResponse;
+ HashSet extraFieldsInResponse = DetermineExtraFieldsInResponse(firstRowProbe, context.FieldsToBeReturned);
uint defaultPageSize = runtimeConfig.DefaultPageSize();
uint maxPageSize = runtimeConfig.MaxPageSize();
+ bool hasNext = isCollection && SqlPaginationUtil.HasNext(findOperationResponse, context.First, defaultPageSize, maxPageSize);
- // If the results are not a collection or if the query does not have a next page
- // no nextLink/after is needed. So, the response is returned after removing the extra fields.
- if (findOperationResponse.ValueKind is not JsonValueKind.Array || !SqlPaginationUtil.HasNext(findOperationResponse, context.First, defaultPageSize, maxPageSize))
+ // No-pagination path: single object, or a collection without a next page.
+ if (!hasNext)
{
- // If there are no additional fields present, the response is returned directly. When there
- // are extra fields, they are removed before returning the response.
if (extraFieldsInResponse.Count == 0)
{
return OkResponse(findOperationResponse);
}
- else
- {
- return findOperationResponse.ValueKind is JsonValueKind.Array ? OkResponse(JsonSerializer.SerializeToElement(RemoveExtraFieldsInResponseWithMultipleItems(findOperationResponse.EnumerateArray().ToList(), extraFieldsInResponse)))
- : OkResponse(RemoveExtraFieldsInResponseWithSingleItem(findOperationResponse, extraFieldsInResponse));
- }
+
+ return isCollection
+ ? OkResponse(JsonSerializer.SerializeToElement(RemoveExtraFieldsInResponseWithMultipleItems(findOperationResponse.EnumerateArray().ToList(), extraFieldsInResponse)))
+ : OkResponse(RemoveExtraFieldsInResponseWithSingleItem(findOperationResponse, extraFieldsInResponse));
}
- List rootEnumerated = findOperationResponse.EnumerateArray().ToList();
+ // Paginated path.
+ List rows = findOperationResponse.EnumerateArray().ToList();
// More records exist than requested, we know this by requesting 1 extra record,
// that extra record is removed here.
- rootEnumerated.RemoveAt(rootEnumerated.Count - 1);
+ rows.RemoveAt(rows.Count - 1);
// The fields such as primary keys, fields in $orderby clause that are retrieved in addition to the
// fields requested in the $select clause are required for calculating the $after element which is part of nextLink.
// So, the extra fields are removed post the calculation of $after element.
string after = SqlPaginationUtil.MakeCursorFromJsonElement(
- element: rootEnumerated[rootEnumerated.Count - 1],
+ element: rows[rows.Count - 1],
orderByColumns: context.OrderByClauseOfBackingColumns,
primaryKey: sqlMetadataProvider.GetSourceDefinition(context.EntityName).PrimaryKey,
entityName: context.EntityName,
@@ -94,40 +94,34 @@ public static OkObjectResult FormatFindResult(
// When there are extra fields present, they are removed before returning the response.
if (extraFieldsInResponse.Count > 0)
{
- rootEnumerated = RemoveExtraFieldsInResponseWithMultipleItems(rootEnumerated, extraFieldsInResponse);
+ rows = RemoveExtraFieldsInResponseWithMultipleItems(rows, extraFieldsInResponse);
}
- // Create an 'after' object if the request comes from MCP endpoint.
+ // MCP endpoint: { value: [...], after: "" }
if (isMcpRequest is true)
{
- string jsonString = JsonSerializer.Serialize(new[]
+ return new OkObjectResult(new
{
- new { after = after }
+ value = rows,
+ after = after
});
- JsonElement afterElement = JsonSerializer.Deserialize(jsonString);
-
- rootEnumerated.Add(afterElement);
}
- // Create a 'nextLink' object if the request comes from REST endpoint.
- else
- {
- string basePaginationUri = SqlPaginationUtil.ConstructBaseUriForPagination(httpContext, runtimeConfig.Runtime?.BaseRoute);
- // Build the query string with the $after token.
- string queryString = SqlPaginationUtil.BuildQueryStringWithAfterToken(
- queryStringParameters: context!.ParsedQueryString,
- newAfterPayload: after);
+ // REST endpoint: { value: [...], nextLink: "" }
+ string basePaginationUri = SqlPaginationUtil.ConstructBaseUriForPagination(httpContext, runtimeConfig.Runtime?.BaseRoute);
+ string queryString = SqlPaginationUtil.BuildQueryStringWithAfterToken(
+ queryStringParameters: context!.ParsedQueryString,
+ newAfterPayload: after);
+ string nextLink = SqlPaginationUtil.BuildNextLinkUri(
+ baseUri: basePaginationUri,
+ queryString: queryString,
+ isNextLinkRelative: runtimeConfig.NextLinkRelative());
- // Get the final consolidated nextLink for the pagination.
- JsonElement nextLink = SqlPaginationUtil.GetConsolidatedNextLinkForPagination(
- baseUri: basePaginationUri,
- queryString: queryString,
- isNextLinkRelative: runtimeConfig.NextLinkRelative());
-
- rootEnumerated.Add(nextLink);
- }
-
- return OkResponse(JsonSerializer.SerializeToElement(rootEnumerated), isMcpRequest);
+ return new OkObjectResult(new
+ {
+ value = rows,
+ nextLink = nextLink
+ });
}
///
@@ -200,60 +194,26 @@ private static JsonElement RemoveExtraFieldsInResponseWithSingleItem(JsonElement
}
///
- /// Helper function returns an OkObjectResult with provided arguments in a
- /// form that complies with vNext Api guidelines.
+ /// Helper function returns an OkObjectResult that wraps a single JsonElement (object or array)
+ /// into the standard { "value": [ ... ] } envelope used by REST/MCP responses.
+ ///
+ /// Pagination metadata (nextLink/after) is intentionally NOT inferred from the
+ /// shape of . attaches those fields
+ /// out-of-band when needed. This avoids confusing array-typed column values (e.g. SQL Server
+ /// JSON arrays, vector/collection types) with a pagination sentinel.
///
/// Value representing the Json results of the client's request.
- /// True if request is done through MCP endpoint.
/// Correctly formatted OkObjectResult.
- public static OkObjectResult OkResponse(JsonElement jsonResult, bool? isMcpRequest = null)
+ public static OkObjectResult OkResponse(JsonElement jsonResult)
{
- // For consistency we return all values as type Array
- if (jsonResult.ValueKind != JsonValueKind.Array)
- {
- string jsonString = $"[{JsonSerializer.Serialize(jsonResult)}]";
- jsonResult = JsonSerializer.Deserialize(jsonString);
- }
+ // For consistency we always return the payload as an array under "value".
+ List rows = jsonResult.ValueKind == JsonValueKind.Array
+ ? jsonResult.EnumerateArray().ToList()
+ : new List { jsonResult };
- List resultEnumerated = jsonResult.EnumerateArray().ToList();
- // More than 0 records, and the last element is of type array, then we have pagination
- if (resultEnumerated.Count > 0 && resultEnumerated[resultEnumerated.Count - 1].ValueKind == JsonValueKind.Array)
- {
- // Get the 'nextLink' or 'after'
- // resultEnumerated will be an array of the form
- // [{object1}, {object2},...{objectlimit}, [{nextLinkObject/afterObject}]]
- // if the last element is of type array, we know it is 'nextLink'
- // if the request is done through the REST endpoint and it is
- // 'after' if the request is done through the MCP endpoint,
- // we strip the "[" and "]" and then save the element
- // into a dictionary with a key of "nextLinkAfter" and a value that
- // represents the nextLink/after data we require.
- string nextLinkAfterJsonString = JsonSerializer.Serialize(resultEnumerated[resultEnumerated.Count - 1]);
- Dictionary nextLinkAfter = JsonSerializer.Deserialize>(nextLinkAfterJsonString[1..^1])!;
- IEnumerable value = resultEnumerated.Take(resultEnumerated.Count - 1);
-
- // Check 'after' object if request is done through MCP endpoint.
- if (isMcpRequest is true)
- {
- return new OkObjectResult(new
- {
- value = value,
- after = nextLinkAfter["after"]
- });
- }
-
- // Check 'nextLink' object if request is done through REST endpoint.
- return new OkObjectResult(new
- {
- value = value,
- @nextLink = nextLinkAfter["nextLink"]
- });
- }
-
- // no pagination, do not need nextLink
return new OkObjectResult(new
{
- value = resultEnumerated
+ value = rows
});
}