Ship Roslyn analyzers inside the EntityGraphQL package - #548
Open
lukemurray wants to merge 3 commits into
Open
Conversation
Nine rules (EGQL001-EGQL009) covering the mistakes that only show up at schema build or under a particular query shape: missing bulk resolvers, DbContext on async fields, collection-only field extensions on scalars, sync Resolve of a Task (with a code fix), invalid GraphQL names, subscriptions that are not observables, non-nullable OneOf fields, duplicate AddField and blocking ExecuteRequest in async methods. Everything is matched by symbol, so the analyzer no-ops in projects that do not reference EntityGraphQL. The example projects reference the analyzers directly to dogfood them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.Result, .Wait() and GetAwaiter().GetResult() written in a Resolve() or ResolveBulk() lambda block the executing thread, once per item on a list. Info rather than a warning: moving to ResolveAsync makes the field resolve concurrently across a list (MaxQueryConcurrency defaults to 100), so the service has to be safe to use that way. Blocking a thread you own is the safer of the two mistakes. Only the resolver's own lambda is scanned - blocking inside a helper it calls is not detected, which keeps the rule cheap and free of guesses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lukemurray
force-pushed
the
feature/analyzers
branch
from
August 18, 2026 06:01
0a9368f to
2486a80
Compare
EGQL001 looked for a ResolveBulk call in the syntax of the statement containing the Resolve call, matched on identifier text. A field defined over several statements through a stored builder - var f = type.AddField(..); f.Resolve(..); f.ResolveBulk(..); - reported the N+1 despite being batched, and a same-named method on an unrelated type silenced it. Resolve the builder to its symbol instead and look for an EntityGraphQL ResolveBulk* call on the same symbol: anywhere in the fluent chain either side of this call, or on the local/field/property/parameter it was stored in. Also tighten EGQL002: any explicit maxConcurrency used to clear it, but only serialising the resolves makes a non-thread-safe service safe - a smaller limit still runs them concurrently. Now only maxConcurrency: 1 clears it, which is what the docs already showed. The literal arrives wrapped in the int-to-int? conversion, so unwrap before reading the constant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds ten Roslyn analyzers (EGQL001–EGQL010) that ship inside the main
EntityGraphQLNuGet package underanalyzers/dotnet/cs, so consumers get them with no extra install.They target the mistakes that are invisible in a simple test: a field works with a single object and no services, then behaves differently once it is selected on a list or once two-pass service execution kicks in. The schema is built in ordinary C#, so an analyzer can see the whole fluent chain and check it.
Rules
.Resolve<TService>()on an entity type with no.ResolveBulk(...)— the N+1 trapDbContext, which resolves concurrently on lists (MaxQueryConcurrencydefaults to 100)UseFilter/UseSort/UseOffsetPaging/UseConnectionPaging/UseAggregateon a non-collection fieldResolve()whose expression returns aTask— has a code fix[GraphQLSubscription]method not returning an observable[GraphQLOneOf]input type with a non-nullable fieldExecuteRequestinside an async methodResolve/ResolveBulkblocking on aTaskwith.Result/.Wait()/GetAwaiter().GetResult()Most mirror an exception EntityGraphQL already throws at schema build, so they cannot break a build that was previously working — they just move the report into the editor. Every rule's severity can be changed or turned off per project in
.editorconfig.Notes
demogained aResolveBulkoncontributedBy, which is exactly what EGQL001 was pointing at.ResolveAsyncmakes the field resolve concurrently across a list (MaxQueryConcurrencydefaults to 100), so the service has to be safe to use that way. Blocking a thread you own is the safer of the two mistakes, so the rule nudges rather than insists.docs/docs/analyzers.md; each rule'shelpLinkUrideep-links to its section.Testing
dotnet test src/tests/EntityGraphQL.Analyzers.Tests— 39 tests, compiling snippets in memory against the real EntityGraphQL and EF Core assemblies. Full solution builds with no new warnings, anddotnet packwas verified to place both analyzer DLLs in the package.🤖 Generated with Claude Code