Skip to content

Conversation

@SimonCropp
Copy link
Owner

@SimonCropp SimonCropp commented Jan 16, 2026

Introduces projection-based field resolution to ensure safe access to navigation properties and enforced through compile-time analysis.

What's New

Roslyn Analyzer for Compile-Time Safety

A new analyzer package (GraphQL.EntityFramework.Analyzers) detects problematic usage patterns at build time:

GQLEF002 Warning

Detects unsafe access to navigation properties in Field().Resolve() / Field().ResolveAsync() calls.

What's Safe:

  • Primary key properties (e.g., context.Source.Id)
  • Foreign key properties (e.g., context.Source.ParentId)

What's Unsafe:

  • Navigation properties (e.g., context.Source.Parent)
  • Scalar properties (e.g., context.Source.Name, context.Source.Age)
  • Properties on navigation properties (e.g., context.Source.Parent.Id)

The analyzer will suggest using projection-based extension methods for unsafe patterns.

GQLEF003 Error

Prevents identity projection (_ => _) in projection-based methods. Identity projection defeats the purpose of the projection system and doesn't load any additional navigation properties.

What to do instead:

  • Use regular Resolve() for PK/FK access
  • Use explicit projection for navigation properties (e.g., x => x.Parent)

Projection-Based FieldBuilder Extensions

New extension methods on FieldBuilder for safe custom resolvers:

Synchronous Resolution

public class ChildGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public ChildGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<int>("ParentId")
            .Resolve<MyDbContext, ChildEntity, int, ParentEntity>(
                projection: x => x.Parent!,
                resolve: ctx => ctx.Projection.Id);
}

snippet source | anchor

Asynchronous Resolution

public class ChildGraphTypeAsync :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public ChildGraphTypeAsync(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<int>("ParentIdAsync")
            .ResolveAsync<MyDbContext, ChildEntity, int, ParentEntity>(
                projection: x => x.Parent!,
                resolve: async ctx => await SomeAsyncOperation(ctx.Projection));

    static Task<int> SomeAsyncOperation(ParentEntity parent) =>
        Task.FromResult(parent.Id);
}

snippet source | anchor

List Resolution

public class ParentGraphTypeList :
    EfObjectGraphType<MyDbContext, ParentEntity>
{
    public ParentGraphTypeList(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<IEnumerable<int>>("ChildIds")
            .ResolveList<MyDbContext, ParentEntity, int, IEnumerable<ChildEntity>>(
                projection: x => x.Children,
                resolve: ctx => ctx.Projection.Select(c => c.Id));
}

snippet source | anchor

These methods provide ResolveProjectionContext<TDbContext, TProjection> with access to:

  • Projection - The projected data
  • DbContext - The current DbContext
  • User - The current user (ClaimsPrincipal)
  • Filters - Global filters
  • FieldContext - The GraphQL field context

Projection-Based Navigation Field Methods

New overloads for navigation fields with built-in projection support.

Single Navigation

Simple - Direct Projection:

public class ChildGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public ChildGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationField<ParentEntity>(
            "parent",
            projection: x => x.Parent);
}

snippet source | anchor

Complex - With Custom Resolver:

public class ChildGraphTypeComplex :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public ChildGraphTypeComplex(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationField<ParentEntity, ParentEntity>(
            "parent",
            projection: x => x.Parent!,
            resolve: ctx => TransformParent(ctx.Projection));

    static ParentEntity TransformParent(ParentEntity parent) => parent;
}

snippet source | anchor

List Navigation

Simple - Direct Projection:

public class ParentGraphTypeListSimple :
    EfObjectGraphType<MyDbContext, ParentEntity>
{
    public ParentGraphTypeListSimple(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationListField<ChildEntity>(
            "children",
            projection: x => x.Children);
}

snippet source | anchor

Complex - With Custom Resolver:

public class ParentGraphTypeListComplex :
    EfObjectGraphType<MyDbContext, ParentEntity>
{
    public ParentGraphTypeListComplex(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationListField<ChildEntity, IEnumerable<ChildEntity>>(
            "children",
            projection: x => x.Children!,
            resolve: ctx => ctx.Projection.OrderBy(c => c.Property));
}

snippet source | anchor

Connection Navigation (Pagination)

Simple - Direct Projection:

public class ParentGraphTypeConnectionSimple :
    EfObjectGraphType<MyDbContext, ParentEntity>
{
    public ParentGraphTypeConnectionSimple(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationConnectionField<ChildEntity>(
            "childrenConnection",
            projection: x => x.Children);
}

snippet source | anchor

Complex - With Custom Resolver:

public class ParentGraphTypeConnectionComplex :
    EfObjectGraphType<MyDbContext, ParentEntity>
{
    public ParentGraphTypeConnectionComplex(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationConnectionField<ChildEntity, IEnumerable<ChildEntity>>(
            "childrenConnection",
            projection: x => x.Children!,
            resolve: ctx => FilterChildren(ctx.Projection));

    static IEnumerable<ChildEntity> FilterChildren(IEnumerable<ChildEntity> children) =>
        children;
}

snippet source | anchor

All new navigation methods properly apply filters and support GraphQL query arguments (where, orderBy, skip, take, ids).

AutoMap Filter Support

AutoMap() now properly applies filters to auto-mapped navigation fields:

  • Collection navigation properties respect global and entity-specific filters
  • Single navigation properties respect filters
  • Works with both AddNavigationField and AddNavigationListField

Migration Guide

Step 1: Update Navigation Fields

Old Style (Deprecated):

// OLD - Deprecated
#pragma warning disable CS0618 // Type or member is obsolete
public class OldChildGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public OldChildGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationField<ParentEntity>(
            "parent",
            resolve: ctx => ctx.Source.Parent,
            includeNames: ["Parent"]);
}
#pragma warning restore CS0618 // Type or member is obsolete

snippet source | anchor

New Style (Projection-based):

// NEW - Projection-based
public class NewChildGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public NewChildGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationField<ParentEntity>(
            "parent",
            projection: x => x.Parent);
}

snippet source | anchor

Step 2: Update Custom Resolvers

Old Style (Unsafe):

// OLD - Unsafe
public class OldCustomResolveGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public OldCustomResolveGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<int>("ParentId")
            .Resolve(ctx => ctx.Source.Parent!.Id); // May be null!
}

snippet source | anchor

New Style (Safe with Projection):

// NEW - Safe with projection
public class NewCustomResolveGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public NewCustomResolveGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<int>("ParentId")
            .Resolve<MyDbContext, ChildEntity, int, ParentEntity>(
                projection: x => x.Parent!,
                resolve: ctx => ctx.Projection.Id);
}

snippet source | anchor

Step 3: Keep Safe PK/FK Access Direct

Primary key and foreign key access doesn't require projection:

// This is still safe without projection - accessing FK directly
public class SafeFKAccessGraphType :
    EfObjectGraphType<MyDbContext, ChildEntity>
{
    public SafeFKAccessGraphType(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<Guid>("ParentId")
            .Resolve(ctx => ctx.Source.ParentId); // Safe - FK always loaded
}

snippet source | anchor

Breaking Changes

Deprecated Methods

The following methods are now marked [Obsolete] and will generate compiler warnings:

  • AddNavigationField<TSource, TReturn>(..., Func<ResolveEfFieldContext, TReturn> resolve, ...)
  • AddNavigationListField<TSource, TReturn>(..., Func<ResolveEfFieldContext, IEnumerable<TReturn>> resolve, ...)
  • AddNavigationConnectionField<TSource, TReturn>(..., Func<ResolveEfFieldContext, IEnumerable<TReturn>> resolve, ...)

These methods will be removed in a future major version. Migrate to projection-based overloads.

Benefits

Compile-Time Safety

The Roslyn analyzer catches navigation property access issues at build time, preventing runtime null reference exceptions.

Better Performance

Explicit projections allow EF Core to generate more efficient SQL queries by only loading required data.

Clearer Intent

Projection expressions clearly document which navigation properties are required for each resolver.

Automatic Filter Application

Projection-based methods automatically apply global and entity-specific filters, ensuring consistent authorization.

Troubleshooting

"IEfGraphQLService not found in request services"

Ensure the DbContext is registered with EfGraphQLConventions.RegisterInContainer<TDbContext>().

"Navigation property is null in resolver"

This occurs when accessing a navigation property without projection. Use the projection-based extension methods or navigation field overloads.

"GQLEF002 warning on valid code"

When only accessing primary keys or foreign keys, the warning is a false positive. The analyzer uses heuristics and may occasionally flag safe code. Options include:

  • Use #pragma warning disable GQLEF002 for specific lines
  • Access the FK property directly instead of navigating (e.g., ctx.Source.ParentId instead of ctx.Source.Parent.Id)

"GQLEF003 error on identity projection"

Don't use _ => _ in projection-based methods. Options:

  • Use regular Resolve() when only needing PK/FK
  • Specify the navigation property: x => x.Parent

Additional Resources

Foreign keys were not being included when building projections for nested navigations (e.g., members in a politicalParty query). This caused FK properties like PoliticalPartyId to be Guid.Empty, leading to 'Sequence contains no elements' errors when custom field resolvers tried to use them.

The fix adds a new section in TryBuildNavigationBindings that explicitly binds foreign key properties, similar to how it's done in BuildExpression for top-level entities.
Tests verify that foreign keys are properly included when querying nested navigation properties. This ensures custom field resolvers that depend on foreign keys work correctly.

The tests cover:
- Custom fields that use foreign keys to query related data
- Foreign key values being non-empty (not Guid.Empty) in nested projections
- Integration with the projection system

These tests demonstrate the bug that was fixed in commit 031931b where foreign keys were missing from nested navigation projections.
@SimonCropp SimonCropp merged commit 55c488b into main Jan 27, 2026
2 of 4 checks passed
@SimonCropp SimonCropp deleted the projection-part-2 branch January 27, 2026 23:01
@SimonCropp SimonCropp changed the title Projection part 2 Introduces projection-based field resolution to ensure safe access to navigation properties and enforced through compile-time analysis. Jan 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants