From 630427a4b62ca4cc1dda3d712753a59129bb307f Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Thu, 7 May 2026 12:27:48 +0200 Subject: [PATCH] refactor(controller): dedupe filter+include logic between `JsonApiQueryAsync` and `BuildJsonApiQueryAsync` --- .../Controllers/JsonApiController.cs | 89 +++++++------------ 1 file changed, 30 insertions(+), 59 deletions(-) diff --git a/JsonApiToolkit/Controllers/JsonApiController.cs b/JsonApiToolkit/Controllers/JsonApiController.cs index 3564661..7e6b426 100644 --- a/JsonApiToolkit/Controllers/JsonApiController.cs +++ b/JsonApiToolkit/Controllers/JsonApiController.cs @@ -197,7 +197,8 @@ string resourceType IQueryable filteredQuery = ApplyFiltersAndIncludes( queryable, parameters, - mappedIncludes + mappedIncludes, + paginating: parameters.Pagination != null ); if (parameters.Sort?.Count > 0) @@ -280,53 +281,15 @@ protected async Task> BuildJsonApiQueryAsync( parameters.Include ); - if (parameters.Include?.Count > 0 && mappedIncludes.Count == 0) - { - Logger.LogWarning( - "No valid includes for {EntityType}. Requested: {Includes}", - typeof(T).Name, - string.Join(", ", parameters.Include) - ); - } + LogInvalidIncludes(parameters, mappedIncludes); - var (mainFilters, includeFilters) = IncludeFilterParser.SeparateIncludeFilters( - parameters.Filter, - parameters.Include + IQueryable processedQuery = ApplyFiltersAndIncludes( + queryable, + parameters, + mappedIncludes, + paginating: false ); - IQueryable processedQuery = queryable; - - // Apply main entity filters - if (mainFilters != null) - processedQuery = processedQuery.ApplyFilters(mainFilters, Logger); - - // Apply includes (with or without filters) - if (includeFilters.Count > 0) - { - Logger.LogDebug( - "Applying {FilterCount} filtered includes for {EntityType}", - includeFilters.Count, - typeof(T).Name - ); - processedQuery = processedQuery.ApplyFilteredIncludes( - mappedIncludes, - includeFilters, - Logger - ); - } - else if (mappedIncludes.Count > 0) - { - // Use standard includes (no pagination optimization needed since we're not paginating) - processedQuery = processedQuery.ApplyIncludes(mappedIncludes); - - Logger.LogDebug( - "Applied {IncludeCount} includes for {EntityType}", - mappedIncludes.Count, - typeof(T).Name - ); - } - - // Apply sorting if (parameters.Sort?.Count > 0) processedQuery = processedQuery.ApplySorting(parameters.Sort, Logger); @@ -435,20 +398,17 @@ private void LogQueryParameters(QueryParameters parameters, List mapp ); } - if (parameters.Include?.Count > 0 && mappedIncludes.Count == 0) - { - Logger.LogWarning( - "No valid includes for {EntityType}. Requested: {Includes}", - typeof(T).Name, - string.Join(", ", parameters.Include) - ); - } + LogInvalidIncludes(parameters, mappedIncludes); } + // When `paginating` is true, includes use single-query mode to avoid the + // EF Core warning/exception triggered by split-query + Skip/Take. Otherwise + // split-query is preferred to avoid cartesian explosion on collection includes. private IQueryable ApplyFiltersAndIncludes( IQueryable queryable, QueryParameters parameters, - List mappedIncludes + List mappedIncludes, + bool paginating ) where T : class { @@ -477,22 +437,33 @@ List mappedIncludes } else if (mappedIncludes.Count > 0) { - filteredQuery = - parameters.Pagination != null - ? filteredQuery.ApplyIncludesSingleQuery(mappedIncludes) - : filteredQuery.ApplyIncludes(mappedIncludes); + filteredQuery = paginating + ? filteredQuery.ApplyIncludesSingleQuery(mappedIncludes) + : filteredQuery.ApplyIncludes(mappedIncludes); Logger.LogDebug( "Applied {IncludeCount} includes for {EntityType} using {QueryType}", mappedIncludes.Count, typeof(T).Name, - parameters.Pagination != null ? "SingleQuery" : "SplitQuery" + paginating ? "SingleQuery" : "SplitQuery" ); } return filteredQuery; } + private void LogInvalidIncludes(QueryParameters parameters, List mappedIncludes) + { + if (parameters.Include?.Count > 0 && mappedIncludes.Count == 0) + { + Logger.LogWarning( + "No valid includes for {EntityType}. Requested: {Includes}", + typeof(T).Name, + string.Join(", ", parameters.Include) + ); + } + } + private static void EnforceStrictPagination( JsonApiOptions options, QueryParameters parameters,