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
4 changes: 2 additions & 2 deletions src/GraphQL.EntityFramework/Filters/FilterEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public FieldProjectionInfo AddRequirements(
var navMetadata = FindNavigation(navigationProperties, navName)!;
mergedNavigations[navName] = mergedNavigations.TryGetValue(navName, out var existingNav)
? existingNav with { IsWhole = true }
: new(navMetadata.Type, navMetadata.IsCollection, new([], null, null, null), true);
: new(navMetadata.Type, navMetadata.IsCollection, new([], null, null, null), true, IsRequired: navMetadata.IsRequired);
}
}

Expand All @@ -135,7 +135,7 @@ public FieldProjectionInfo AddRequirements(
}
else
{
mergedNavigations[navName] = new(navMetadata.Type, navMetadata.IsCollection, new(requiredProps, null, null, null));
mergedNavigations[navName] = new(navMetadata.Type, navMetadata.IsCollection, new(requiredProps, null, null, null), IsRequired: navMetadata.IsRequired);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,9 @@ FieldType BuildFirstField<TSource, TReturn>(
return ReturnNullable();
}

if (disableTracking)
{
query = query.AsNoTracking();
}

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

query = includeAppender.ApplyProjection(context, fieldContext.Filters, query);
query = includeAppender.ApplyProjection(context, fieldContext.Filters, query, disableTracking);

QueryLogger.Write(query);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,12 @@ FieldType BuildQueryField<TSource, TReturn>(
return Array.Empty<TReturn>();
}

if (disableTracking)
{
query = query.AsNoTracking();
}

if (!omitQueryArguments)
{
query = query.ApplyGraphQlArguments(context, names, true, omitQueryArguments);
}

query = includeAppender.ApplyProjection(context, fieldContext.Filters, query);
query = includeAppender.ApplyProjection(context, fieldContext.Filters, query, disableTracking);

QueryLogger.Write(query);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,9 @@ ConnectionBuilder<TSource> BuildQueryConnection<TSource, TReturn>(
return Empty(context);
}

if (disableTracking)
{
query = query.AsNoTracking();
}

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

query = includeAppender.ApplyProjection(context, fieldContext.Filters, query);
query = includeAppender.ApplyProjection(context, fieldContext.Filters, query, disableTracking);

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,9 @@ FieldType BuildSingleField<TSource, TReturn>(
return ReturnNullable();
}

if (disableTracking)
{
query = query.AsNoTracking();
}

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

query = includeAppender.ApplyProjection(context, fieldContext.Filters, query);
query = includeAppender.ApplyProjection(context, fieldContext.Filters, query, disableTracking);

QueryLogger.Write(query);

Expand Down
21 changes: 15 additions & 6 deletions src/GraphQL.EntityFramework/IncludeAppender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
/// where the entity can be projected, otherwise includes. A projected navigation whose type
/// cannot be projected is bound whole, and the navigations under it are then loaded through
/// includes alongside the select, since EF applies includes to the entities in a projection.
/// <paramref name="disableTracking"/> applies AsNoTracking only when no select is added, since
/// a select creates new instances that EF does not track, so AsNoTracking would do nothing.
/// </summary>
public IQueryable<TItem> ApplyProjection<TDbContext, TItem>(
IResolveFieldContext context,
Filters<TDbContext>? filters,
IQueryable<TItem> query)
IQueryable<TItem> query,
bool disableTracking = false)
where TDbContext : DbContext
where TItem : class
{
if (context.SubFields is null)
{
return query;
return ApplyTracking(query, disableTracking);
}

var type = typeof(TItem);
Expand All @@ -43,7 +46,7 @@ public IQueryable<TItem> ApplyProjection<TDbContext, TItem>(

if (!SelectExpressionBuilder.TryBuild<TItem>(projection, keyNames, derivedTypes, out var expression, out var includePaths, out var argumentFields))
{
return AddIncludesFromProjection(query, projection);
return ApplyTracking(AddIncludesFromProjection(query, projection), disableTracking);
}

foreach (var includePath in includePaths)
Expand All @@ -56,6 +59,10 @@ public IQueryable<TItem> ApplyProjection<TDbContext, TItem>(
return query.Select(expression);
}

static IQueryable<TItem> ApplyTracking<TItem>(IQueryable<TItem> query, bool disableTracking)
where TItem : class =>
disableTracking ? query.AsNoTracking() : query;

/// <summary>
/// Whether the projection loads a collection at any depth, the only case where query splitting
/// changes anything.
Expand Down Expand Up @@ -432,7 +439,8 @@ void ProcessSelectionSet(
new(
navType,
navigation.IsCollection,
GetNestedProjection(field.SelectionSet, navGraphType, navType, nestedNavProps, nestedKeys, nestedFks, context)));
GetNestedProjection(field.SelectionSet, navGraphType, navType, nestedNavProps, nestedKeys, nestedFks, context),
IsRequired: navigation.IsRequired));
}

return result;
Expand Down Expand Up @@ -686,7 +694,7 @@ void ProcessProjectionExpression(
nestedProjection = new(nestedScalarFields, nestedKeys ?? [], nestedFks ?? new HashSet<string>(), []);
}

AddNavigation(navProjections, navigation.Name, new(navType, navigation.IsCollection, nestedProjection, isWhole, arguments));
AddNavigation(navProjections, navigation.Name, new(navType, navigation.IsCollection, nestedProjection, isWhole, arguments, navigation.IsRequired));
}
}

Expand Down Expand Up @@ -891,7 +899,8 @@ void ProcessNavigationOrScalar(
new(
navType,
navigation.IsCollection,
GetNestedProjection(field.SelectionSet, GetComplexGraphType(fieldType), navType, nestedNavProps, nestedKeys, nestedFks, context)));
GetNestedProjection(field.SelectionSet, GetComplexGraphType(fieldType), navType, nestedNavProps, nestedKeys, nestedFks, context),
IsRequired: navigation.IsRequired));
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQL.EntityFramework/Navigation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public record Navigation
Type Type,
bool IsNullable,
bool IsCollection,
string? InverseName = null
string? InverseName = null,
bool IsRequired = false
);
10 changes: 9 additions & 1 deletion src/GraphQL.EntityFramework/NavigationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ static IReadOnlyDictionary<string, Navigation> GetNavigations(IEntityType entity
_ =>
{
var (itemType, isCollection) = GetNavigationType(_);
return new Navigation(_.Name, itemType, _.PropertyInfo!.IsNullable(), isCollection, _.Inverse?.Name);
return new Navigation(_.Name, itemType, _.PropertyInfo!.IsNullable(), isCollection, _.Inverse?.Name, IsRequired(_));
})
.ToDictionary(_ => _.Name.ToLowerInvariant(), StringComparer.OrdinalIgnoreCase);
}

// EF never materializes a required reference navigation as null, so the select projection
// can skip the null check on it
static bool IsRequired(INavigationBase navigation) =>
navigation is INavigation { IsCollection: false } reference &&
(reference.IsOnDependent
? reference.ForeignKey.IsRequired
: reference.ForeignKey.IsRequiredDependent);

static (Type itemType, bool isCollection) GetNavigationType(INavigationBase navigation)
{
var navigationType = navigation.ClrType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
/// itself rather than properties of it. The select projection then binds the navigation whole
/// instead of building a member init from <paramref name="Projection"/>.
/// </param>
/// <param name="IsRequired">
/// The navigation is required in the EF model, so is never null. The select projection then
/// skips the null check, which EF would otherwise translate to an always false comparison.
/// </param>
/// <param name="Arguments">
/// The field's ids, where and orderBy, to apply inside the collection subquery. Null when the
/// navigation was selected more than once, since one loaded collection cannot satisfy two sets
Expand All @@ -13,7 +17,8 @@ record NavigationProjectionInfo(
bool IsCollection,
FieldProjectionInfo Projection,
bool IsWhole = false,
NavigationArguments? Arguments = null)
NavigationArguments? Arguments = null,
bool IsRequired = false)
{
public NavigationProjectionInfo Merge(NavigationProjectionInfo other) =>
this with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ static bool TryBuildNavigationBinding(
return false;
}

// A required navigation is never null, and EF translates a null check on one to an always
// false comparison, so it is only checked when optional
if (navProjection.IsRequired)
{
binding = Expression.Bind(navAccess.Member, init);
return true;
}

// source.Parent == null ? null : new Parent { ... }
var conditional = Expression.Condition(
Expression.Equal(navAccess, navMetadata.NullConstant),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
},
sql: {
Text:
select top (2) cast (0 as bit),
case when f0.Discriminator = N'FilterDerivedEntity' then cast (1 as bit) else cast (0 as bit) end,
select top (2) case when f0.Discriminator = N'FilterDerivedEntity' then cast (1 as bit) else cast (0 as bit) end,
f0.CommonProperty,
f.BaseEntityId,
f.Id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
},
sql: {
Text:
select top (2) cast (0 as bit),
case when f0.Discriminator = N'FilterDerivedEntity' then cast (1 as bit) else cast (0 as bit) end,
select top (2) case when f0.Discriminator = N'FilterDerivedEntity' then cast (1 as bit) else cast (0 as bit) end,
f0.CommonProperty,
f.BaseEntityId,
f.Id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
},
sql: {
Text:
select cast (0 as bit),
d.Id,
select d.Id,
d.IsActive,
d.Name,
e.DepartmentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
},
sql: {
Text:
select top (2) cast (0 as bit),
case when f0.Discriminator = N'FilterDerivedEntity' then cast (1 as bit) else cast (0 as bit) end,
select top (2) case when f0.Discriminator = N'FilterDerivedEntity' then cast (1 as bit) else cast (0 as bit) end,
f0.CommonProperty,
f0.Id,
f.BaseEntityId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
},
sql: {
Text:
select case when c.Id is null then cast (1 as bit) else cast (0 as bit) end,
c.Id,
select c.Id,
c.ParentId,
c0.Id,
c0.ParentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
},
sql: {
Text:
select top (2) cast (0 as bit),
o.Child1_Property,
select top (2) o.Child1_Property as Property,
o.Id,
o.Property
from OwnedParents as o
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
sql: {
Text:
select i0.Id,
case when i1.Id is null then cast (1 as bit) else cast (0 as bit) end,
i1.Id
from IncludeNonQueryableBs as i
inner join
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
},
sql: {
Text:
select case when c.Id is null then cast (1 as bit) else cast (0 as bit) end,
c.Id,
select c.Id,
case when w0.Id is null then cast (1 as bit) else cast (0 as bit) end,
w0.Id,
c.ParentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
},
sql: {
Text:
select top (2) cast (0 as bit),
o.Child1_Property,
select top (2) o.Child1_Property as Property,
o.Id,
o.Property
from OwnedParents as o
Expand Down
Loading