Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;NU5104;CS1573;CS9107;NU1608;NU1109</NoWarn>
<Version>34.1.6</Version>
<Version>34.1.7</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ FieldType BuildFirstField<TSource, TReturn>(
// Get filter-required fields early so we can add filter-required navigations via Include
var allFilterFields = fieldContext.Filters?.GetAllRequiredFilterProperties();

query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context);
query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context, allFilterFields);
query = query.ApplyGraphQlArguments(context, names, false, omitQueryArguments);

// Apply column projection based on requested GraphQL fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ FieldType BuildQueryField<TSource, TReturn>(
// Get filter-required fields early so we can add filter-required navigations via Include
var allFilterFields = fieldContext.Filters?.GetAllRequiredFilterProperties();

query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context);
query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context, allFilterFields);
if (!omitQueryArguments)
{
query = query.ApplyGraphQlArguments(context, names, true, omitQueryArguments);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ ConnectionBuilder<TSource> AddQueryableConnection<TSource, TGraph, TReturn>(
// Get filter-required fields early so we can add filter-required navigations via Include
var allFilterFields = fieldContext.Filters?.GetAllRequiredFilterProperties();

query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context);
query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context, allFilterFields);
query = query.ApplyGraphQlArguments(context, names, true, omitQueryArguments);

// Apply column projection based on requested GraphQL fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ FieldType BuildSingleField<TSource, TReturn>(
// Get filter-required fields early so we can add filter-required navigations via Include
var allFilterFields = fieldContext.Filters?.GetAllRequiredFilterProperties();

query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context);
query = includeAppender.AddIncludesWithFiltersAndDetectNavigations(query, context, allFilterFields);
query = query.ApplyGraphQlArguments(context, names, false, omitQueryArguments);

// Apply column projection based on requested GraphQL fields
Expand Down
86 changes: 80 additions & 6 deletions src/GraphQL.EntityFramework/IncludeAppender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,94 @@ public IQueryable<TItem> AddIncludesWithFilters<TItem>(
IResolveFieldContext context,
IReadOnlyDictionary<Type, IReadOnlySet<string>>? allFilterFields)
where TItem : class =>
AddIncludesWithFiltersAndDetectNavigations(query, context);
AddIncludesWithFiltersAndDetectNavigations(query, context, allFilterFields);

internal IQueryable<TItem> AddIncludesWithFiltersAndDetectNavigations<TItem>(
IQueryable<TItem> query,
IResolveFieldContext context)
IResolveFieldContext context,
IReadOnlyDictionary<Type, IReadOnlySet<string>>? allFilterFields = null)
where TItem : class
{
// Add includes from GraphQL query
query = AddIncludes(query, context);

// Note: Filter-required navigations are now handled entirely by the projection system
// in TryGetProjectionExpressionWithFilters, which builds projections that include
// filter-required fields. Abstract navigation access is prevented by FilterEntry validation.
// No additional includes are needed here.
// Add includes for filter-required navigations.
// While projection handles most cases, abstract navigation types cannot be projected
// (can't do "new AbstractType { ... }"). In those cases, Include is needed as fallback.
if (allFilterFields is { Count: > 0 })
{
query = AddFilterNavigationIncludes(query, allFilterFields, typeof(TItem));
}

return query;
}

IQueryable<TItem> AddFilterNavigationIncludes<TItem>(
IQueryable<TItem> query,
IReadOnlyDictionary<Type, IReadOnlySet<string>> allFilterFields,
Type entityType)
where TItem : class
{
// Get filter fields for this entity type and its base types
var relevantFilterFields = new List<string>();
foreach (var (filterType, filterFields) in allFilterFields)
{
if (filterType.IsAssignableFrom(entityType))
{
relevantFilterFields.AddRange(filterFields);
}
}

if (relevantFilterFields.Count == 0)
{
return query;
}

// Get navigation metadata for this entity type
if (!navigations.TryGetValue(entityType, out var navigationProperties))
{
return query;
}

// Extract unique navigation names from filter fields (paths like "TravelRequest.Status" -> "TravelRequest")
var navigationNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var field in relevantFilterFields)
{
if (field.Contains('.'))
{
var navName = field[..field.IndexOf('.')];
navigationNames.Add(navName);
}
}

// Add Include for each filter-required navigation that has an abstract type
// (Non-abstract navigations can be handled by projection)
foreach (var navName in navigationNames)
{
// Find navigation in metadata (case-insensitive)
Navigation? navMetadata = null;
string? actualNavName = null;
foreach (var (key, value) in navigationProperties)
{
if (string.Equals(key, navName, StringComparison.OrdinalIgnoreCase))
{
navMetadata = value;
actualNavName = key;
break;
}
}

if (navMetadata == null || actualNavName == null)
{
continue;
}

// Only add Include for abstract types - concrete types can use projection
if (navMetadata.Type.IsAbstract)
{
query = query.Include(navMetadata.Name);
}
}

return query;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
target:
{
"data": {
"derivedChildEntities": [
{
"id": "Guid_1",
"property": "Child1"
}
]
}
},
sql: {
Text:
select d.Id,
d.ParentId,
d.Property,
d.TypedParentId,
b.Id,
b.Discriminator,
b.Property,
b.Status
from DerivedChildEntities as d
left outer join
BaseEntities as b
on d.ParentId = b.Id
order by d.Property
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
target:
{
"data": {
"derivedChildEntities": [
{
"id": "Guid_1",
"property": "Child1-Approved"
}
]
}
},
sql: {
Text:
select d.Id,
d.ParentId,
d.Property,
d.TypedParentId,
b.Id,
b.Discriminator,
b.Property,
b.Status
from DerivedChildEntities as d
left outer join
BaseEntities as b
on d.ParentId = b.Id
order by d.Property
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
target:
{
"data": {
"derivedChildEntities": [
{
"id": "Guid_1",
"property": "Child1"
}
]
}
},
sql: {
Text:
select d.Id,
d.ParentId,
d.Property,
d.TypedParentId,
b.Id,
b.Discriminator,
b.Property,
b.Status
from DerivedChildEntities as d
left outer join
BaseEntities as b
on d.ParentId = b.Id
order by d.Property
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
target:
{
"data": {
"derivedChildEntities": [
{
"id": "Guid_1",
"property": "Child1"
}
]
}
},
sql: {
Text:
select d.Id,
d.ParentId,
d.Property,
d.TypedParentId,
b.Id,
b.Discriminator,
b.Property,
b.Status
from DerivedChildEntities as d
left outer join
BaseEntities as b
on d.ParentId = b.Id
order by d.Property
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
target:
{
"data": {
"derivedChildEntities": [
{
"id": "Guid_1",
"property": "ChildWithParent"
}
]
}
},
sql: {
Text:
select d.Id,
d.ParentId,
d.Property,
d.TypedParentId,
b.Id,
b.Discriminator,
b.Property,
b.Status
from DerivedChildEntities as d
left outer join
BaseEntities as b
on d.ParentId = b.Id
order by d.Property
}
}
Loading