Skip to content

Latest commit

 

History

History
88 lines (63 loc) · 8.32 KB

File metadata and controls

88 lines (63 loc) · 8.32 KB

Linq2GraphQL.Client — High-Level Repo Report

Analysed at main @ a17c22b (2026-09-07)

1. What the repo is

A LINQ-to-GraphQL client for .NET, shipped as three NuGet artifacts:

Artifact Kind Role
Linq2GraphQL.Generator dotnet tool (Linq2GraphQL) Introspects a GraphQL endpoint, emits a strongly-typed C# client via T4 templates
Linq2GraphQL.Client library Runtime: turns Include/Select lambdas into a query tree, executes, deserializes
Linq2GraphQL.Client.Subscriptions library Optional subscriptions transport (graphql-ws + SSE)

Everything else in the tree — test/, docs/, StartGG/ — exists to exercise or demonstrate those three.

2. Size and shape

Hand-written C# (excluding bin/obj):

src/Linq2GraphQL.Generator            6 701 lines  (32 files)  <- mostly T4 output
src/Linq2GraphQL.Client               2 584 lines  (45 files)
src/Linq2GraphQL.Client.Subscriptions   383 lines  ( 9 files)
test/Linq2GraphQL.Tests               2 190 lines  (19 files, 126 [Fact]/[Theory])
test/Linq2GraphQL.TestClient*         6 523 lines  (checked-in generated output)
docs/ + StartGG/                     15 465 lines  (Blazor site + sample clients)

The product itself is small — under 3 000 lines of runtime code. That is the headline: a compact, focused library where the generator's line count is inflated by machine-generated template partials, not by complexity.

Largest runtime files: Converters/EnumConverter.cs (343), Visitors/QueryExpressionVisitor.cs (332), QueryNode.cs (244), Utilities.cs (185).

3. Architecture in one paragraph

Three stages, and understanding them is usually the whole job:

  1. Build — generated QueryMethods/MutationMethods return GraphQuery<T> seeded with a root QueryNode (field name + ArgumentValues).
  2. Parse — Include(...)/Select(...) lambdas go through Utilities.ParseExpression -> QueryExpressionVisitor. Two modes: ResolvePath for expressions that name a field (member chains, [GraphQLMember] methods, LINQ operators), and plain ExpressionVisitor walking for everything else so every mentioned field still lands in the query. Lambda parameters bind to nodes keyed by ParameterExpression reference, so nested lambdas reusing a name stay distinct.
  3. Execute — GraphBaseExecute lazily assigns unique variable names and auto-adds primitive children, renders query text from the QueryNode tree; QueryExecutor<T> POSTs and unwraps data/errors/extensions.

QueryNode is the entire intermediate representation. Everything interesting happens between it and the visitor.

4. Highlights — the good

  • The test strategy is unusually strong for a library this size. 126 tests, and most are genuinely end-to-end: WebApplicationFactory<Program> boots a real HotChocolate server in-process (Linq2GraphQL.TestServer) and the checked-in generated client queries it. A passing suite proves the generator, the parser, the query text and the deserializer all agree — not just that a unit returns the expected string.
  • The nullable variant is a first-class citizen, not an afterthought: a parallel TestServerNullable + TestClientNullable + fixture pair.
  • Build hygiene is modern and tight. .NET 10 throughout, central package management (Directory.Packages.props), RestorePackagesWithLockFile with --locked-mode in CI, Nerdbank.GitVersioning driving versions from git, and solution filters splitting CI (Linq2GraphQL.CI.slnf) from pack/publish (Linq2GraphQL.Release.slnf). No hand-edited version numbers anywhere.
  • Release is one button. release.yml is workflow_dispatch-only: nbgv -> pack -> push to NuGet -> GitHub release. Low ceremony, hard to trigger by accident.
  • Deliberate design details that show maturity: argument-hash-derived GraphQL aliases so the same field can be requested twice with different arguments; opt-in "safe mode" that validates auto-included primitives against a cached introspection result; two error surfaces (ExecuteAsync throws, ExecuteWithResultAsync returns GraphResult<T>) funnelled through a single ProcessResponseFull.
  • Generated files are normalised to LF (ReplaceLineEndings("\n") in Program.cs) — a small thing that prevents a lot of cross-platform diff noise.

5. Highlights — the risks

  • T4 is the sharpest edge in the repo. Fixed. Templates are now preprocessed at build time by the pinned dotnet-t4 local tool into gitignored X.g.cs files, so a .tt edit takes effect on the next dotnet build on any platform and stale template logic cannot be built. (Switching it on revealed one already-dormant edit: ScalarTemplate.tt referenced a non-existent GraphqlType.ScalarTypeName and had never been regenerated.)
  • Generated test clients are checked in but never regenerated by the build. Fixed. ./scripts/regenerate-test-clients.ps1 boots both test servers over plain HTTP and regenerates TestClient/TestClientNullable, and the generated-clients CI job runs it with -Check so committed output that no longer matches the generator fails the build. Refreshing the output picked up three pieces of accumulated drift: the newly active template edits, the signed Byte scalar mapping (byte → sbyte), and the raiseError/raiseAuthError query methods.
  • Coupling between alias hashing and deserialization. Utilities.GetArgumentsId must produce the same hash at write time and read time. Any change to argument hashing breaks reads as well as writes, and the failure mode is a silently missing field rather than an exception.
  • Ambient static generator state. GeneratorSettings.Current is read from inside templates, so nullable/non-nullable output depends on global mutable state rather than a passed parameter. Fine today; awkward if generation ever needs to run concurrently.
  • docs/ and StartGG/ are outside the CI filter and can drift. They are in Linq2GraphQL.sln but not Linq2GraphQL.CI.slnf, so nothing verifies they still compile — roughly 15k lines CI never touches.
  • Subscriptions are only partly covered. Two transports exist (WSClient for graphql-ws, SSEClient for SSE), but only SSE works under the test host — and there is exactly 1 subscription test. The WebSocket path is effectively untested.
  • Minor tidy-ups: nuget.config.backup is committed alongside nuget.config; a stray root-level StarWars.Client/ containing only obj/ shadows the real docs/StarWars.Client; the README options list has a few typos (--nullabel, "Exprimental").

6. Activity and ownership

  • 255 commits, active since 2023: 110 (2023), 60 (2024), 73 (2025), 12 (2026 YTD).
  • Concentrated ownership. Joakim Dangården holds 180 of 255 commits across two identities; Magnus Ahlberg 62; a long tail of 4 outside contributors with ~19 between them. Bus factor is effectively one.
  • Recent work is squarely in the core. The last six months of churn lands on src/Linq2GraphQL.Client (10 touches, 5 of them in Visitors/), Subscriptions (7), and the tests (6). Notable recent commits: the expression parser rewrite (#92), the .NET 10 upgrade with CVE/warning cleanup (#90), and structured GraphQL error handling contributed externally (#88).
  • PR-driven workflow. Nearly every change lands as a merge commit from a named branch — the history reads cleanly.

7. If I were picking up work here

  1. Wire up CLI T4 regeneration — done: dotnet-t4 runs from the generator's csproj on every build, and the preprocessed output is no longer checked in.
  2. Add a CI job that regenerates the test clients and diffs them — done: scripts/regenerate-test-clients.ps1 -Check, run by the generated-clients job.
  3. Pull docs/ and StartGG/ into a build-only CI job so they cannot rot unnoticed.
  4. Get the WebSocket subscription transport under test, even against a standalone host outside WebApplicationFactory.
  5. Delete nuget.config.backup and the stray root StarWars.Client/.

Appendix — commands that matter

# What CI runs
dotnet restore Linq2GraphQL.CI.slnf --locked-mode
dotnet build   Linq2GraphQL.CI.slnf --no-restore
dotnet test    Linq2GraphQL.CI.slnf --no-build

# Generate a client against a live endpoint
dotnet run --project src/Linq2GraphQL.Generator -- <endpoint> -c=ClientName -n=Namespace -o=Generated