From 9099449972a5234de868d6b2b5803ac59b77df1f Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Fri, 27 Mar 2026 12:45:58 +0100 Subject: [PATCH] fix: resolve all 36 open CodeQL code scanning alerts --- .../Extensions/IncludeFilterParserTests.cs | 15 ++---- .../Extensions/QueryableExtensionTests.cs | 9 +--- .../Integration/JsonApiQueryAsyncTests.cs | 11 ++-- .../Mapping/JsonApiMapperTests.cs | 22 ++++---- .../Mapping/JsonColumnMappingTests.cs | 15 +++--- .../Parsing/JsonApiQueryParserTests.cs | 8 +-- .../Configuration/QueryComplexityAnalyzer.cs | 43 ++++++--------- .../Extensions/QueryableExtensions.cs | 8 +-- .../Filtering/FilterExpressionBuilder.cs | 49 ++++++----------- .../Filtering/FilterOperatorExpressions.cs | 2 +- .../Querying/Filtering/IncludeFilterParser.cs | 54 +++++++------------ .../Querying/Helpers/QueryHelpers.cs | 7 +++ .../Includes/FilteredIncludeBuilder.cs | 18 +++---- JsonApiToolkit/Helpers/EfIncludePathHelper.cs | 5 +- JsonApiToolkit/Mapping/EntityMapper.cs | 37 +++++++------ JsonApiToolkit/Parsing/JsonApiQueryParser.cs | 10 ++-- JsonApiToolkit/Validation/IncludeValidator.cs | 30 ++++------- 17 files changed, 139 insertions(+), 204 deletions(-) diff --git a/JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs b/JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs index 369842d..e212ce8 100644 --- a/JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs +++ b/JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs @@ -165,10 +165,7 @@ public void SeparateIncludeFilters_WithKebabCaseInclude_HandlesCorrectly() var includePaths = new List { "cve-comments" }; // Act - var (mainFilters, includeFilters) = IncludeFilterParser.SeparateIncludeFilters( - filters, - includePaths - ); + var (_, includeFilters) = IncludeFilterParser.SeparateIncludeFilters(filters, includePaths); // Assert Assert.Single(includeFilters); @@ -197,10 +194,7 @@ public void SeparateIncludeFilters_WithNestedIncludeFilter_SeparatesCorrectly() var includePaths = new List { "comments.author" }; // Act - var (mainFilters, includeFilters) = IncludeFilterParser.SeparateIncludeFilters( - filters, - includePaths - ); + var (_, includeFilters) = IncludeFilterParser.SeparateIncludeFilters(filters, includePaths); // Assert Assert.Single(includeFilters); @@ -237,10 +231,7 @@ public void SeparateIncludeFilters_WithComplexOrFilter_HandlesCorrectly() var includePaths = new List { "comments" }; // Act - var (mainFilters, includeFilters) = IncludeFilterParser.SeparateIncludeFilters( - filters, - includePaths - ); + var (_, includeFilters) = IncludeFilterParser.SeparateIncludeFilters(filters, includePaths); // Assert Assert.Single(includeFilters); diff --git a/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs b/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs index 9488e15..6066e63 100644 --- a/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs +++ b/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs @@ -226,10 +226,7 @@ public void ApplySorting_WithMultipleSorts_SortsCorrectly() .ToList() .Select(e => { - if (e.Id == 1 || e.Id == 3) - e.IsActive = true; - else - e.IsActive = false; + e.IsActive = e.Id == 1 || e.Id == 3; return e; }) .AsQueryable(); @@ -314,10 +311,6 @@ public async Task CreatePaginationMetaAsync_WithInvalidPageNumber_ReturnsLastPag { var query = GetTestData(); var pagination = new PaginationParameters { Number = 10, Size = 2 }; - var totalCount = query.Count(); - var totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size); - var expectedCurrentPage = Math.Min(Math.Max(pagination.Number, 1), Math.Max(totalPages, 1)); - var meta = await query.CreatePaginationMetaAsync(pagination); Assert.Equal(5, meta.TotalResources); diff --git a/JsonApiToolkit.Tests/Integration/JsonApiQueryAsyncTests.cs b/JsonApiToolkit.Tests/Integration/JsonApiQueryAsyncTests.cs index a327521..8300256 100644 --- a/JsonApiToolkit.Tests/Integration/JsonApiQueryAsyncTests.cs +++ b/JsonApiToolkit.Tests/Integration/JsonApiQueryAsyncTests.cs @@ -506,20 +506,15 @@ public async Task GetArticles_WithPagination_ReturnsMetadataAsync() Assert.Equal(2, GetPaginationValue(document.Meta, "pageSize")); } - private static T GetPaginationValue(Dictionary meta, string key) + private static int GetPaginationValue(Dictionary meta, string key) { if ( meta.TryGetValue("pagination", out var pagination) && pagination is JsonElement paginationElement + && paginationElement.TryGetProperty(key, out var property) ) { - if (paginationElement.TryGetProperty(key, out var property)) - { - if (typeof(T) == typeof(int)) - return (T)(object)property.GetInt32(); - if (typeof(T) == typeof(string)) - return (T)(object)property.GetString()!; - } + return property.GetInt32(); } throw new InvalidOperationException( diff --git a/JsonApiToolkit.Tests/Mapping/JsonApiMapperTests.cs b/JsonApiToolkit.Tests/Mapping/JsonApiMapperTests.cs index 8ccc2c0..55663a6 100644 --- a/JsonApiToolkit.Tests/Mapping/JsonApiMapperTests.cs +++ b/JsonApiToolkit.Tests/Mapping/JsonApiMapperTests.cs @@ -46,8 +46,10 @@ public void ToResourceObject_IncludesForeignKeyIdsInAttributes() // Foreign key IDs should be included in attributes Assert.NotNull(resourceObject.Attributes); - Assert.True(resourceObject.Attributes.ContainsKey("relatedEntityId")); - Assert.Equal(42, resourceObject.Attributes["relatedEntityId"]); + Assert.True( + resourceObject.Attributes.TryGetValue("relatedEntityId", out var relatedEntityIdValue) + ); + Assert.Equal(42, relatedEntityIdValue); } [Fact] @@ -70,11 +72,11 @@ public void ToResourceObject_WithRelationships_MapsRelationshipsCorrectly() ); Assert.NotNull(resourceObject.Relationships); - Assert.True(resourceObject.Relationships.ContainsKey("relatedEntity")); - - Relationship relationship = resourceObject.Relationships["relatedEntity"]; + Assert.True( + resourceObject.Relationships.TryGetValue("relatedEntity", out var relationship) + ); ResourceIdentifier resourceIdentifier = Assert.IsType( - relationship.Data + relationship!.Data ); Assert.Equal("2", resourceIdentifier.Id); Assert.Equal("testRelatedEntity", resourceIdentifier.Type); @@ -93,9 +95,7 @@ public void ToResourceObject_WithCollectionRelationship_MapsCollectionCorrectly( var resourceObject = JsonApiMapper.ToResourceObject(entity, "testEntities", ["Children"]); Assert.NotNull(resourceObject.Relationships); - Assert.True(resourceObject.Relationships.ContainsKey("children")); - - Relationship relationship = resourceObject.Relationships["children"]; + Assert.True(resourceObject.Relationships.TryGetValue("children", out var relationship)); IEnumerable identifiers = Assert.IsAssignableFrom< IEnumerable >(relationship.Data); @@ -365,8 +365,8 @@ public void ToResourceObject_ExcludesJsonIgnoreProperties() var resourceObject = JsonApiMapper.ToResourceObject(entity, "entities"); Assert.NotNull(resourceObject.Attributes); - Assert.True(resourceObject.Attributes.ContainsKey("visibleName")); - Assert.Equal("Visible", resourceObject.Attributes["visibleName"]); + Assert.True(resourceObject.Attributes.TryGetValue("visibleName", out var visibleNameValue)); + Assert.Equal("Visible", visibleNameValue); Assert.False(resourceObject.Attributes.ContainsKey("secretPassword")); Assert.False(resourceObject.Attributes.ContainsKey("internalData")); } diff --git a/JsonApiToolkit.Tests/Mapping/JsonColumnMappingTests.cs b/JsonApiToolkit.Tests/Mapping/JsonColumnMappingTests.cs index 03aa5ae..895a0f2 100644 --- a/JsonApiToolkit.Tests/Mapping/JsonColumnMappingTests.cs +++ b/JsonApiToolkit.Tests/Mapping/JsonColumnMappingTests.cs @@ -133,21 +133,22 @@ public void ToResourceObject_MapsJsonColumnsAsAttributes() Assert.Equal("Test Entity", resource.Attributes["name"]); // JSON columns should be in attributes - Assert.True(resource.Attributes.ContainsKey("jsonDataList")); - Assert.True(resource.Attributes.ContainsKey("exploitationReports")); - Assert.True(resource.Attributes.ContainsKey("complexData")); + Assert.True(resource.Attributes.TryGetValue("jsonDataList", out var jsonDataListValue)); + Assert.True( + resource.Attributes.TryGetValue("exploitationReports", out var exploitationReportsValue) + ); + Assert.True(resource.Attributes.TryGetValue("complexData", out var complexDataValue)); // Verify the JSON data is preserved - var jsonDataList = resource.Attributes["jsonDataList"] as List; + var jsonDataList = jsonDataListValue as List; Assert.NotNull(jsonDataList); Assert.Equal(2, jsonDataList.Count); - var exploitationReports = - resource.Attributes["exploitationReports"] as ICollection; + var exploitationReports = exploitationReportsValue as ICollection; Assert.NotNull(exploitationReports); Assert.Single(exploitationReports); - var complexData = resource.Attributes["complexData"] as ComplexJsonData; + var complexData = complexDataValue as ComplexJsonData; Assert.NotNull(complexData); Assert.Equal("Security", complexData.Category); Assert.Equal(2, complexData.Tags.Count); diff --git a/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs b/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs index e321a36..0c065b0 100644 --- a/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs +++ b/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs @@ -393,10 +393,10 @@ public void Parse_WithSingleFieldset_ParsesCorrectly() Assert.NotNull(parameters.Fields); Assert.Single(parameters.Fields); - Assert.True(parameters.Fields.ContainsKey("articles")); - Assert.Equal(2, parameters.Fields["articles"].Count); - Assert.Contains("title", parameters.Fields["articles"]); - Assert.Contains("content", parameters.Fields["articles"]); + Assert.True(parameters.Fields.TryGetValue("articles", out var articlesFields)); + Assert.Equal(2, articlesFields!.Count); + Assert.Contains("title", articlesFields); + Assert.Contains("content", articlesFields); } [Fact] diff --git a/JsonApiToolkit/Configuration/QueryComplexityAnalyzer.cs b/JsonApiToolkit/Configuration/QueryComplexityAnalyzer.cs index 9b36ff4..437f801 100644 --- a/JsonApiToolkit/Configuration/QueryComplexityAnalyzer.cs +++ b/JsonApiToolkit/Configuration/QueryComplexityAnalyzer.cs @@ -125,37 +125,28 @@ public static int GetMaxDepth(FilterGroup group, int currentDepth = 1) if (group.Groups.Count == 0) return currentDepth; - int maxChildDepth = currentDepth; - foreach (var nested in group.Groups) - { - int childDepth = GetMaxDepth(nested, currentDepth + 1); - if (childDepth > maxChildDepth) - maxChildDepth = childDepth; - } - return maxChildDepth; + return group.Groups.Select(nested => GetMaxDepth(nested, currentDepth + 1)).Max(); } private static void ValidateFilterValueLengths(FilterGroup group, int maxLength) { - foreach (var filter in group.Filters) + var tooLong = group.Filters.FirstOrDefault(f => f.Value?.Length > maxLength); + if (tooLong != null) { - if (filter.Value?.Length > maxLength) - { - throw new JsonApiBadRequestException( - $"Filter value for '{filter.Field}' is {filter.Value.Length} characters, " - + $"but maximum allowed is {maxLength}. " - + "Reduce value length or configure a higher limit via JsonApiOptions.MaxFilterValueLength.", - JsonApiErrorCodes.QueryTooComplex, - new ErrorSource { Parameter = $"filter[{filter.Field}]" }, - new Dictionary - { - ["field"] = filter.Field, - ["valueLength"] = filter.Value.Length, - ["limit"] = maxLength, - ["configKey"] = "JsonApiOptions.MaxFilterValueLength", - } - ); - } + throw new JsonApiBadRequestException( + $"Filter value for '{tooLong.Field}' is {tooLong.Value!.Length} characters, " + + $"but maximum allowed is {maxLength}. " + + "Reduce value length or configure a higher limit via JsonApiOptions.MaxFilterValueLength.", + JsonApiErrorCodes.QueryTooComplex, + new ErrorSource { Parameter = $"filter[{tooLong.Field}]" }, + new Dictionary + { + ["field"] = tooLong.Field, + ["valueLength"] = tooLong.Value!.Length, + ["limit"] = maxLength, + ["configKey"] = "JsonApiOptions.MaxFilterValueLength", + } + ); } foreach (var nested in group.Groups) diff --git a/JsonApiToolkit/Extensions/QueryableExtensions.cs b/JsonApiToolkit/Extensions/QueryableExtensions.cs index ff093a6..f39ae33 100644 --- a/JsonApiToolkit/Extensions/QueryableExtensions.cs +++ b/JsonApiToolkit/Extensions/QueryableExtensions.cs @@ -24,10 +24,10 @@ QueryParameters parameters if (parameters.Filter != null) query = query.ApplyFilters(parameters.Filter); - if (parameters.Sort?.Count > 0) - query = query.ApplySorting(parameters.Sort); - else - query = query.ApplySorting([new SortParameter { Field = "Id", IsDescending = false }]); + query = + parameters.Sort?.Count > 0 + ? query.ApplySorting(parameters.Sort!) + : query.ApplySorting([new SortParameter { Field = "Id", IsDescending = false }]); if (parameters.Pagination != null) query = query.ApplyPagination(parameters.Pagination); diff --git a/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs b/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs index 53c855c..630a354 100644 --- a/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs +++ b/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs @@ -69,17 +69,11 @@ public static class FilterExpressionBuilder } } - foreach (FilterGroup nestedGroup in group.Groups) - { - Expression? nestedExpr = BuildFilterExpression( - nestedGroup, - parameter, - entityType, - logger - ); - if (nestedExpr != null) - expressions.Add(nestedExpr); - } + expressions.AddRange( + group + .Groups.Select(g => BuildFilterExpression(g, parameter, entityType, logger)) + .OfType() + ); if (expressions.Count == 0) return null; @@ -96,32 +90,23 @@ public static class FilterExpressionBuilder if (group.LogicalOperator == LogicalOperator.Not) { - foreach (Expression expr in expressions) - { - var notExpr = Expression.Not(expr); - combinedExpression = - combinedExpression == null - ? notExpr - : Expression.OrElse(combinedExpression, notExpr); - } + combinedExpression = expressions + .Select(e => (Expression)Expression.Not(e)) + .Aggregate((acc, next) => Expression.OrElse(acc, next)); } else { foreach (Expression expr in expressions) { - if (combinedExpression == null) - { - combinedExpression = expr; - } - else - { - combinedExpression = group.LogicalOperator switch - { - LogicalOperator.And => Expression.AndAlso(combinedExpression, expr), - LogicalOperator.Or => Expression.OrElse(combinedExpression, expr), - _ => Expression.AndAlso(combinedExpression, expr), - }; - } + combinedExpression = + combinedExpression == null + ? expr + : group.LogicalOperator switch + { + LogicalOperator.And => Expression.AndAlso(combinedExpression, expr), + LogicalOperator.Or => Expression.OrElse(combinedExpression, expr), + _ => Expression.AndAlso(combinedExpression, expr), + }; } } diff --git a/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs b/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs index 7230040..c4f2fd6 100644 --- a/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs +++ b/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs @@ -76,7 +76,7 @@ Type propertyType if (converted != null) convertedValues.Add(converted); } - catch (Exception) + catch (FormatException) { failedValues.Add(rawValue); } diff --git a/JsonApiToolkit/Extensions/Querying/Filtering/IncludeFilterParser.cs b/JsonApiToolkit/Extensions/Querying/Filtering/IncludeFilterParser.cs index 9829368..58951c4 100644 --- a/JsonApiToolkit/Extensions/Querying/Filtering/IncludeFilterParser.cs +++ b/JsonApiToolkit/Extensions/Querying/Filtering/IncludeFilterParser.cs @@ -106,27 +106,19 @@ out var fieldPath } // Process nested groups - foreach (var nestedGroup in group.Groups) - { - var processedNestedGroup = ExtractIncludeFilters( - nestedGroup, - normalizedIncludePaths, - filtersByRelationship - ); - - if ( - processedNestedGroup != null - && (processedNestedGroup.Filters.Count > 0 || processedNestedGroup.Groups.Count > 0) - ) - { - newGroup.Groups.Add(processedNestedGroup); - } - } + newGroup.Groups.AddRange( + group + .Groups.Select(g => + ExtractIncludeFilters(g, normalizedIncludePaths, filtersByRelationship) + ) + .OfType() + .Where(g => g.Filters.Count > 0 || g.Groups.Count > 0) + ); // Merge local include filters into the global dictionary foreach (var kvp in localIncludeFilters) { - if (!filtersByRelationship.ContainsKey(kvp.Key)) + if (!filtersByRelationship.TryGetValue(kvp.Key, out var existing)) { // First time seeing this relationship - use its logical operator directly filtersByRelationship[kvp.Key] = kvp.Value; @@ -134,7 +126,6 @@ out var fieldPath else { // Already have filters for this relationship - need to merge - var existing = filtersByRelationship[kvp.Key]; // If both groups have the same operator and no nested groups, merge filters if ( @@ -230,19 +221,11 @@ out string fieldPath return false; } - private static HashSet NormalizeIncludePaths(List includePaths) - { - var normalized = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var path in includePaths) - { - // Convert kebab-case to camelCase for comparison - var normalizedPath = ConvertKebabToCamelCase(path); - normalized.Add(normalizedPath); - } - - return normalized; - } + private static HashSet NormalizeIncludePaths(List includePaths) => + new HashSet( + includePaths.Select(ConvertKebabToCamelCase), + StringComparer.OrdinalIgnoreCase + ); private static string ConvertKebabToCamelCase(string kebabCase) { @@ -256,19 +239,18 @@ private static string ConvertKebabToCamelCase(string kebabCase) return part; var segments = part.Split('-'); - var result = segments[0].ToLowerInvariant(); + var sb = new System.Text.StringBuilder(segments[0].ToLowerInvariant()); for (int i = 1; i < segments.Length; i++) { if (segments[i].Length > 0) { - result += - char.ToUpperInvariant(segments[i][0]) - + segments[i].Substring(1).ToLowerInvariant(); + sb.Append(char.ToUpperInvariant(segments[i][0])); + sb.Append(segments[i].Substring(1).ToLowerInvariant()); } } - return result; + return sb.ToString(); }); return string.Join(".", convertedParts); diff --git a/JsonApiToolkit/Extensions/Querying/Helpers/QueryHelpers.cs b/JsonApiToolkit/Extensions/Querying/Helpers/QueryHelpers.cs index dcd5156..d789ae7 100644 --- a/JsonApiToolkit/Extensions/Querying/Helpers/QueryHelpers.cs +++ b/JsonApiToolkit/Extensions/Querying/Helpers/QueryHelpers.cs @@ -135,6 +135,13 @@ public static class QueryHelpers ); } catch (Exception ex) + when (ex + is FormatException + or OverflowException + or InvalidCastException + or ArgumentException + or NotSupportedException + ) { throw new FormatException( $"Failed to convert filter value '{value}' to type '{targetType.FullName}'. " diff --git a/JsonApiToolkit/Extensions/Querying/Includes/FilteredIncludeBuilder.cs b/JsonApiToolkit/Extensions/Querying/Includes/FilteredIncludeBuilder.cs index a7ecc1a..8c30435 100644 --- a/JsonApiToolkit/Extensions/Querying/Includes/FilteredIncludeBuilder.cs +++ b/JsonApiToolkit/Extensions/Querying/Includes/FilteredIncludeBuilder.cs @@ -43,17 +43,11 @@ public static IQueryable ApplyFilteredIncludes( { var segments = includePath.Split('.'); - if ( + query = filtersByRelationship.TryGetValue(includePath, out var filterGroup) && (filterGroup.Filters.Count > 0 || filterGroup.Groups.Count > 0) - ) - { - query = ApplyFilteredIncludeChain(query, segments, filterGroup, typeof(T), logger); - } - else - { - query = query.Include(includePath); - } + ? ApplyFilteredIncludeChain(query, segments, filterGroup, typeof(T), logger) + : query.Include(includePath); } return query; @@ -191,6 +185,12 @@ private static IQueryable ApplyTwoLevelFilteredInclude( } } catch (Exception ex) + when (ex + is InvalidOperationException + or TargetInvocationException + or ArgumentException + or NotSupportedException + ) { logger?.LogWarning( ex, diff --git a/JsonApiToolkit/Helpers/EfIncludePathHelper.cs b/JsonApiToolkit/Helpers/EfIncludePathHelper.cs index 7be104f..0dc20fb 100644 --- a/JsonApiToolkit/Helpers/EfIncludePathHelper.cs +++ b/JsonApiToolkit/Helpers/EfIncludePathHelper.cs @@ -23,11 +23,8 @@ public static List MapIncludePathsToClrProperties(List? inclu var type = typeof(T); var mapped = new List(includePaths.Count); - foreach (var path in includePaths) + foreach (var path in includePaths.Where(p => !string.IsNullOrWhiteSpace(p))) { - if (string.IsNullOrWhiteSpace(path)) - continue; - var mappedPath = s_includePathCache.GetOrAdd( (type, path), key => MapSinglePath(key.Item1, key.Item2) diff --git a/JsonApiToolkit/Mapping/EntityMapper.cs b/JsonApiToolkit/Mapping/EntityMapper.cs index e5fe1c3..a381abc 100644 --- a/JsonApiToolkit/Mapping/EntityMapper.cs +++ b/JsonApiToolkit/Mapping/EntityMapper.cs @@ -81,31 +81,30 @@ public static List GetRelationshipProperties(Type type) .Where(p => p.CanRead && p.GetMethod?.IsPublic == true - && !HasJsonIgnoreAttribute(p) // Exclude properties marked with [JsonIgnore] - && ( - ( - typeof(IEnumerable).IsAssignableFrom(p.PropertyType) - && p.PropertyType != typeof(string) - && HasIdProperty(GetCollectionElementType(p.PropertyType)) // Only include collections where items have IDs - ) - || ( - !p.PropertyType.IsPrimitive - && !p.PropertyType.IsValueType - && p.PropertyType != typeof(string) - && p.PropertyType != typeof(DateTime) - && p.PropertyType != typeof(DateTime?) - && p.PropertyType != typeof(Guid) - && p.PropertyType != typeof(Guid?) - && !typeof(IEnumerable).IsAssignableFrom(p.PropertyType) // Exclude collections from complex object relationships - && HasIdProperty(p.PropertyType) // Only include single objects that have ID properties as relationships - ) - ) + && !HasJsonIgnoreAttribute(p) + && (IsCollectionRelationship(p) || IsSingleObjectRelationship(p)) ) .ToList(); } ); } + private static bool IsCollectionRelationship(PropertyInfo p) => + typeof(IEnumerable).IsAssignableFrom(p.PropertyType) + && p.PropertyType != typeof(string) + && HasIdProperty(GetCollectionElementType(p.PropertyType)); + + private static bool IsSingleObjectRelationship(PropertyInfo p) => + !p.PropertyType.IsPrimitive + && !p.PropertyType.IsValueType + && p.PropertyType != typeof(string) + && p.PropertyType != typeof(DateTime) + && p.PropertyType != typeof(DateTime?) + && p.PropertyType != typeof(Guid) + && p.PropertyType != typeof(Guid?) + && !typeof(IEnumerable).IsAssignableFrom(p.PropertyType) + && HasIdProperty(p.PropertyType); + /// /// Gets the JSON:API resource type name (entity class name in camelCase). /// Example: "Person" becomes "person". diff --git a/JsonApiToolkit/Parsing/JsonApiQueryParser.cs b/JsonApiToolkit/Parsing/JsonApiQueryParser.cs index cdabd2d..7e3ed5f 100644 --- a/JsonApiToolkit/Parsing/JsonApiQueryParser.cs +++ b/JsonApiToolkit/Parsing/JsonApiQueryParser.cs @@ -115,11 +115,13 @@ public static QueryParameters Parse( if (request.Query.TryGetValue("sort", out StringValues sortValue)) { var sortParams = new List(); - foreach (string field in sortValue.ToString().Split(',')) + foreach ( + string field in sortValue + .ToString() + .Split(',') + .Where(f => !string.IsNullOrWhiteSpace(f)) + ) { - if (string.IsNullOrWhiteSpace(field)) - continue; - bool isDescending = field.StartsWith(char.ToString('-')); string fieldName = isDescending ? field.Substring(1) : field; diff --git a/JsonApiToolkit/Validation/IncludeValidator.cs b/JsonApiToolkit/Validation/IncludeValidator.cs index 6f6c3d2..b8505aa 100644 --- a/JsonApiToolkit/Validation/IncludeValidator.cs +++ b/JsonApiToolkit/Validation/IncludeValidator.cs @@ -17,15 +17,7 @@ IEnumerable allowedPatterns ) { var patterns = allowedPatterns.Select(p => new IncludePattern(p)).ToList(); - var forbidden = new List(); - - foreach (var requested in requestedIncludes) - { - if (!IsIncludeAllowed(requested, patterns)) - { - forbidden.Add(requested); - } - } + var forbidden = requestedIncludes.Where(r => !IsIncludeAllowed(r, patterns)).ToList(); return new ValidationResult { @@ -65,17 +57,17 @@ private static void CollectForbiddenFilterPaths( List forbidden ) { - foreach (var filter in group.Filters) + foreach ( + var relationshipPath in group + .Filters.Select(f => ExtractRelationshipPath(f.Field)) + .OfType() + .Where(path => + !IsIncludeAllowed(path, patterns) + && !forbidden.Contains(path, StringComparer.OrdinalIgnoreCase) + ) + ) { - var relationshipPath = ExtractRelationshipPath(filter.Field); - if (relationshipPath != null && !IsIncludeAllowed(relationshipPath, patterns)) - { - // Only add if not already in the list - if (!forbidden.Contains(relationshipPath, StringComparer.OrdinalIgnoreCase)) - { - forbidden.Add(relationshipPath); - } - } + forbidden.Add(relationshipPath); } foreach (var nestedGroup in group.Groups)