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>33.0.1</Version>
<Version>33.1.0</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQL.EntityFramework/GraphApi/EfGraphQLService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public EfGraphQLService(
keyNames = model.GetKeyNames();

Navigations = NavigationReader.GetNavigationProperties(model);
includeAppender = new(Navigations);
includeAppender = new(Navigations, keyNames);
}

public IReadOnlyDictionary<Type, IReadOnlyList<Navigation>> Navigations { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ FieldType BuildQueryField<TSource, TReturn>(
query = query.ApplyGraphQlArguments(context, names, true, omitQueryArguments);
}

// Apply column projection based on requested GraphQL fields
var projection = includeAppender.GetProjection<TReturn>(context);
if (projection != null)
{
var selectExpr = SelectExpressionBuilder.Build<TReturn>(projection, keyNames);
query = query.Select(selectExpr);
}

QueryLogger.Write(query);

List<TReturn> list;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ ConnectionBuilder<TSource> AddQueryableConnection<TSource, TGraph, TReturn>(
query = includeAppender.AddIncludes(query, context);
query = query.ApplyGraphQlArguments(context, names, true, omitQueryArguments);

// Apply column projection based on requested GraphQL fields
var projection = includeAppender.GetProjection<TReturn>(context);
if (projection != null)
{
var selectExpr = SelectExpressionBuilder.Build<TReturn>(projection, keyNames);
query = query.Select(selectExpr);
}

try
{
return await query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ FieldType BuildSingleField<TSource, TReturn>(
query = includeAppender.AddIncludes(query, context);
query = query.ApplyGraphQlArguments(context, names, false, omitQueryArguments);

// Apply column projection based on requested GraphQL fields
var projection = includeAppender.GetProjection<TReturn>(context);
if (projection != null)
{
var selectExpr = SelectExpressionBuilder.Build<TReturn>(projection, keyNames);
query = query.Select(selectExpr);
}

QueryLogger.Write(query);

TReturn? single;
Expand Down
253 changes: 251 additions & 2 deletions src/GraphQL.EntityFramework/IncludeAppender.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class IncludeAppender(IReadOnlyDictionary<Type, IReadOnlyList<Navigation>> navigations)
class IncludeAppender(
IReadOnlyDictionary<Type, IReadOnlyList<Navigation>> navigations,
IReadOnlyDictionary<Type, List<string>> keyNames)
{
public IQueryable<TItem> AddIncludes<TItem>(IQueryable<TItem> query, IResolveFieldContext context)
where TItem : class
Expand All @@ -17,6 +19,253 @@ public IQueryable<TItem> AddIncludes<TItem>(IQueryable<TItem> query, IResolveFie
return AddIncludes(query, context, navigationProperty);
}

public FieldProjectionInfo? GetProjection<TItem>(IResolveFieldContext context)
where TItem : class
{
if (context.SubFields is null)
{
return null;
}

var type = typeof(TItem);
navigations.TryGetValue(type, out var navigationProperties);
keyNames.TryGetValue(type, out var keys);

return GetProjectionInfo(context, navigationProperties, keys ?? []);
}

FieldProjectionInfo GetProjectionInfo(
IResolveFieldContext context,
IReadOnlyList<Navigation>? navigationProperties,
List<string> keys)
{
var scalarFields = new List<string>();
var navProjections = new Dictionary<string, NavigationProjectionInfo>();

if (context.SubFields is not null)
{
foreach (var (fieldName, fieldInfo) in context.SubFields)
{
// Handle connection wrapper fields (edges, items, node)
if (IsConnectionNodeName(fieldName))
{
ProcessConnectionNodeFields(fieldInfo.Field.SelectionSet, navigationProperties, scalarFields, navProjections, context);
}
else
{
ProcessProjectionField(fieldName, fieldInfo, navigationProperties, scalarFields, navProjections, context);
}
}
}

return new(scalarFields, keys, navProjections);
}

void ProcessConnectionNodeFields(
GraphQLSelectionSet? selectionSet,
IReadOnlyList<Navigation>? navigationProperties,
List<string> scalarFields,
Dictionary<string, NavigationProjectionInfo> navProjections,
IResolveFieldContext context)
{
if (selectionSet?.Selections is null)
{
return;
}

foreach (var selection in selectionSet.Selections.OfType<GraphQLField>())
{
var fieldName = selection.Name.StringValue;

// Recursively handle nested connection nodes (e.g., edges -> node)
if (IsConnectionNodeName(fieldName))
{
ProcessConnectionNodeFields(selection.SelectionSet, navigationProperties, scalarFields, navProjections, context);
}
else
{
// Process as regular field
ProcessNestedProjectionField(fieldName, selection, navigationProperties, scalarFields, navProjections, context);
}
}
}

static bool IsConnectionNodeName(string fieldName) =>
fieldName.Equals("edges", StringComparison.OrdinalIgnoreCase) ||
fieldName.Equals("items", StringComparison.OrdinalIgnoreCase) ||
fieldName.Equals("node", StringComparison.OrdinalIgnoreCase);

void ProcessProjectionField(
string fieldName,
(GraphQLField Field, FieldType FieldType) fieldInfo,
IReadOnlyList<Navigation>? navigationProperties,
List<string> scalarFields,
Dictionary<string, NavigationProjectionInfo> navProjections,
IResolveFieldContext context)
{
// Check if this field has include metadata (navigation field with possible alias)
if (TryGetIncludeMetadata(fieldInfo.FieldType, out var includeNames))
{
// It's a navigation field - include ALL navigation properties from metadata
var addedAny = false;
foreach (var navName in includeNames)
{
var navigation = navigationProperties?.FirstOrDefault(n =>
n.Name.Equals(navName, StringComparison.OrdinalIgnoreCase));

if (navigation != null && !navProjections.ContainsKey(navigation.Name))
{
var navType = navigation.Type;
navigations.TryGetValue(navType, out var nestedNavProps);
keyNames.TryGetValue(navType, out var nestedKeys);

// Only the first (primary) navigation gets the nested projection from the query
// Other navigations get empty projections (select all their keys)
var nestedProjection = !addedAny
? GetNestedProjection(
fieldInfo.Field.SelectionSet,
nestedNavProps,
nestedKeys ?? [],
context)
: new([], nestedKeys ?? [], []);

navProjections[navigation.Name] = new(
navType,
navigation.IsCollection,
nestedProjection);
addedAny = true;
}
}

if (addedAny)
{
return;
}
}

// Check if this field is a navigation property by name
var navByName = navigationProperties?.FirstOrDefault(n =>
n.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));

if (navByName != null)
{
// It's a navigation - build nested projection
var navType = navByName.Type;
navigations.TryGetValue(navType, out var nestedNavProps);
keyNames.TryGetValue(navType, out var nestedKeys);

var nestedProjection = GetNestedProjection(
fieldInfo.Field.SelectionSet,
nestedNavProps,
nestedKeys ?? [],
context);

navProjections[navByName.Name] = new(
navType,
navByName.IsCollection,
nestedProjection);
}
else
{
// It's a scalar field
scalarFields.Add(fieldName);
}
}

FieldProjectionInfo GetNestedProjection(
GraphQLSelectionSet? selectionSet,
IReadOnlyList<Navigation>? navigationProperties,
List<string> keys,
IResolveFieldContext context)
{
var scalarFields = new List<string>();
var navProjections = new Dictionary<string, NavigationProjectionInfo>();

if (selectionSet?.Selections is null)
{
return new(scalarFields, keys, navProjections);
}

// Process direct fields
foreach (var selection in selectionSet.Selections.OfType<GraphQLField>())
{
var fieldName = selection.Name.StringValue;
ProcessNestedProjectionField(fieldName, selection, navigationProperties, scalarFields, navProjections, context);
}

// Process inline fragments
foreach (var inlineFragment in selectionSet.Selections.OfType<GraphQLInlineFragment>())
{
foreach (var selection in inlineFragment.SelectionSet.Selections.OfType<GraphQLField>())
{
var fieldName = selection.Name.StringValue;
ProcessNestedProjectionField(fieldName, selection, navigationProperties, scalarFields, navProjections, context);
}
}

// Process fragment spreads
foreach (var fragmentSpread in selectionSet.Selections.OfType<GraphQLFragmentSpread>())
{
var name = fragmentSpread.FragmentName.Name;
var fragmentDefinition = context.Document.Definitions
.OfType<GraphQLFragmentDefinition>()
.SingleOrDefault(_ => _.FragmentName.Name == name);

if (fragmentDefinition?.SelectionSet.Selections is null)
{
continue;
}

foreach (var selection in fragmentDefinition.SelectionSet.Selections.OfType<GraphQLField>())
{
var fieldName = selection.Name.StringValue;
ProcessNestedProjectionField(fieldName, selection, navigationProperties, scalarFields, navProjections, context);
}
}

return new(scalarFields, keys, navProjections);
}

void ProcessNestedProjectionField(
string fieldName,
GraphQLField field,
IReadOnlyList<Navigation>? navigationProperties,
List<string> scalarFields,
Dictionary<string, NavigationProjectionInfo> navProjections,
IResolveFieldContext context)
{
// Check if this field is a navigation property
var navigation = navigationProperties?.FirstOrDefault(n =>
n.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));

if (navigation != null)
{
// It's a navigation - build nested projection
var navType = navigation.Type;
navigations.TryGetValue(navType, out var nestedNavProps);
keyNames.TryGetValue(navType, out var nestedKeys);

var nestedProjection = GetNestedProjection(
field.SelectionSet,
nestedNavProps,
nestedKeys ?? [],
context);

navProjections[navigation.Name] = new(
navType,
navigation.IsCollection,
nestedProjection);
}
else
{
// It's a scalar field - avoid duplicates
if (!scalarFields.Contains(fieldName, StringComparer.OrdinalIgnoreCase))
{
scalarFields.Add(fieldName);
}
}
}

IQueryable<T> AddIncludes<T>(IQueryable<T> query, IResolveFieldContext context, IReadOnlyList<Navigation> navigationProperties)
where T : class
{
Expand Down Expand Up @@ -155,4 +404,4 @@ static bool TryGetIncludeMetadata(FieldType fieldType, [NotNullWhen(true)] out s
value = null;
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace GraphQL.EntityFramework;

record FieldProjectionInfo(
List<string> ScalarFields,
List<string> KeyNames,
Dictionary<string, NavigationProjectionInfo> Navigations);

record NavigationProjectionInfo(
Type EntityType,
bool IsCollection,
FieldProjectionInfo Projection);
Loading