Skip to content

fix(csharp): resolve a member access to the receiver type's property, so DbSet query sites link (#3528) - #3538

Open
Ayushraj06-bit wants to merge 2 commits into
Graphify-Labs:v8from
Ayushraj06-bit:fix/csharp-member-access-3528
Open

Ayushraj06-bit wants to merge 2 commits into
Graphify-Labs:v8from
Ayushraj06-bit:fix/csharp-member-access-3528

Conversation

@Ayushraj06-bit

@Ayushraj06-bit Ayushraj06-bit commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3528.

In short: a C# property read on a typed receiver (db.Users in db.Users.Where(...), this.Count, Config.Instance) now resolves to the receiver type's property node as a uses edge (context=member_access), the same way recv.Method() resolves to a method since #1609. EF Core query sites become reachable from their DbSet property. It changes 2 source files, adds 20 tests, and is rebased on current v8 with CI green. The effect on the graph is additive: on CleanArchitecture, edges go 956 → 992, all 36 new ones are uses, and the node count is unchanged.

The gap

An EF Core query site never names its table: db.Users.Where(...) reaches AspNetUsers through the DbSet<ApplicationUser> Users property, and only the property is in source. Users has been a node since #3006, but the only edge into it was its own type's defines. The C# invocation branch in walk_calls keeps a simple receiver only, so the chained db.Users was dropped, every db.<DbSet>.X() raw call arrived at _resolve_csharp_member_calls with receiver=None, and was skipped. On the reporter's corpus that is why explain "AppDbContext" shows 83 declaration edges and not one use.

Minimal repro (three files, --code-only): .CountOrders() containing db.Orders.Count() has zero outgoing edges; Users/Orders have exactly one inbound edge each.

The fix

recv.Prop now resolves the way recv.Method() has since #1609:

is_member_call: True is kept on access entries so every bare-name pass (shared cross-file, symbol_resolution) skips them exactly as it skips a receiver call; the C# resolver branches on is_member_access.

After the fix, explain "Users" on the repro lists .FindByEmail() L20, .CountOrders() L25, .AddUser() L30 — the "what code reads or writes this table" answer. ApplicationUser is one hop further via the declaration's existing references/generic_arg.

Measured

jasontaylordev/CleanArchitecture (76 .cs): 956 → 992 edges, node count unchanged. All 36 new edges are uses; 12 land on IApplicationDbContext.TodoItems/TodoLists from the command handlers and validators — every EF query site in the project — the rest are request.Title / _user.Id DTO reads on in-corpus types. No wrong binding on inspection.

Tests

tests/test_csharp_member_access.py, 20 tests. 11 fail without the change (the DbSet repro; access line/confidence; bare var x = db.Users and foreach; this.field.Prop; this.Prop and Type.Prop EXTRACTED; base.Prop; interface-typed receiver; inherited property; partial-class half; dedup; outer-chain/generic exclusion; raw-entry shape). 8 are guards that must stay green: untyped lambda parameter, out-of-corpus type (and not parked — flipping the guard fails this test), ambiguous type, field/method-group members, shadowing local poison, same-named C++ member, no dead raw entry for an untypable receiver. The 20th pins the raw-entry split between a callee and a property read (db.Save(); db.Log(db.Users); db.Users.Add(db.Orders.First()) → accesses Orders, Users, Users; calls Add, First, Log, Save).

Full suite, rebased on v8 b57cf91 (Windows 11, Python 3.11): 70 failed / 6307 passed / 146 skipped before, 70 failed / 6327 passed / 146 skipped after. The failing set is identical and all pre-existing Windows failures; none are C# tests. CI passes on 3.10/3.12/3.13/3.14. ruff clean under the committed selection; tools.skillgen --check OK.

Alternatives considered

  • Gate on ORM vocabulary (DbSet, Model, Repository) — hard-codes framework names into a language extractor that has none, needs the property's declared type plumbed to the resolver, and loses the general answer (impact analysis on an entity column is a property read on the entity, not on the DbSet).
  • A node per call site, as the issue words it — this graph's model is nodes = declarations and call sites = edges carrying source_location; thousands of per-occurrence nodes would fight every id/dedup/explain assumption.
  • A separate resolver module like csharp_dispatch.py — would duplicate ~100 lines of receiver typing.

How to verify

pytest tests/test_csharp_member_access.py -q
graphify extract <any EF Core repo> --code-only --no-cluster && graphify explain "<a DbSet property name>"

Deliberately out of scope (issue items 2–4)

entity → table via [Table]/ToTable; explain preferring table vocabulary over a same-named local (a query/explain ranking change); DDL inside migrations (#2884's territory). Also left alone: making the property node carry its own generic_arg to the entity (would double every C# property's type edges; interacts with #3037), enum-member access, property-level interface dispatch, and unqualified Users.Add() inside the context itself.

One pre-existing wrong edge surfaced while reproducing, not touched here: a chained-receiver call (_context.TodoItems.Count()) falls through to the in-file bare-name match and binds Count to the enclosing method's own .Count() — a false self-recursion edge. Fixing it removes some currently-correct chained-receiver edges, so it wants its own PR/decision.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

Resolves a C# member access on a typed receiver (db.Users, this.Count, Config.Instance) to the receiver type's property node as a uses edge with member_access context, mirroring how recv.Method() resolves to a method, so ORM query sites behind a DbSet<T> property become reachable even when the table name never appears in source. Types the receiver through the same tier logic via a new property_index restricted to .cs-declared defines targets to avoid colliding with same-named C++ members, and skips parking for accesses since parked entries are cross-repo call candidates. Extracts the receiver-name logic into _csharp_member_receiver, which returns the bare name for identifiers, this/base, and this.field, and bails on any other chain.

No blocking issues surfaced. 5 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2404 functions depend on the 685 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • …and 38 more — each is listed as a finding

Verification — 2404 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2229 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

276 of 276 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • tests/test_csharp_enum_members.py — impact, full-run-safety
  • … and 226 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

· 46 more finding(s) on lines outside this diff (see the check run).

@Ayushraj06-bit
Ayushraj06-bit force-pushed the fix/csharp-member-access-3528 branch from d4c9f77 to ab92f98 Compare September 13, 2026 18:40

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds C# member-access resolution so a typed receiver read like db.Users, this.Count, or Config.Instance binds to the receiver type's property node as a uses edge, mirroring how recv.Method() resolves to a method. This makes ORM query sites behind a DbSet<T> property reachable even though the table name never appears in source, since the property node is now the link. Property targets are restricted to .cs-declared members to keep bare-name-typed receivers from reaching same-named C++ data members, and member accesses are never parked (parked entries are cross-repo call candidates only). Refactors the shared type-on-bases walk into _member_on_type_or_bases, parameterized over method_index or the new property_index, and extracts C# receiver-name detection into _csharp_member_receiver.

No blocking issues surfaced. 4 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2404 functions depend on the 685 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • …and 38 more — each is listed as a finding

Verification — 2404 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2229 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

276 of 276 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • tests/test_csharp_enum_members.py — impact, full-run-safety
  • … and 226 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_resolve\_csharp\_member\_calls.

The verifier did not have enough to check \_resolve\_csharp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 46 more finding(s) on lines outside this diff (see the check run).

@Ayushraj06-bit

Ayushraj06-bit commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Follow-up commit pushed (5f090fa). Nothing changes in the graph output — verified byte-identical nodes/edges on jasontaylordev/CleanArchitecture (110 .cs) and jellyfin/jellyfin (2,208 .cs) before and after, on both this base and current v8 (0.9.64).

Changelog. The entry was written under ## 0.9.61 (unreleased); merged into today's v8 it lands inside the released 0.9.61 section — the same shape 9335cee had to clean up. Dropped the hunk; happy to leave the wording for the bump.

Perf (jellyfin: +26.6k access entries on top of 85.6k member calls, +6,347 uses edges):

  • engine.py — the access branch asked tree-sitter for node.parent (twice) to tell an invocation's callee from a property read. Node.parent re-descends from the root each call: ~700 ms over the corpus's 137k member_access_expression nodes vs ~13 ms for a set lookup. The walk is pre-order, so the invocation branch now records the callee node's id in a per-file csharp_callee_ids set and the access branch tests membership. Per-file extraction overhead over base: +1.2–1.6 s → +0.7–0.8 s. A new raw-entry test pins the callee/access split (db.Save(); db.Log(db.Users); db.Users.Add(db.Orders.First()) → accesses Orders, Users, Users; calls Add, First, Log, Save).
  • extract.py — _resolve_type_name_nid is memoized per (type name, caller, file); a method reads and calls the same receiver many times over and every access is one more entry that types it (80k → 40k scope-chain walks). The defines scan that builds property_index is folded into the existing method scan — one pass over all_edges fills both twin indexes. _resolve_csharp_member_calls: 430 ms → 288 ms (base 250 ms).

Also checked against current v8: the branch rebases cleanly, full suite there has the same pre-existing Windows/env failure set as v8 itself, 20/20 in tests/test_csharp_member_access.py, ruff clean, tools.skillgen --check OK. Happy to rebase if you'd prefer that over "Update branch".

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds C# member-access resolution: raw db.Users/this.Count accesses stamped is_member_access are typed by the same tier logic as calls and bound to the receiver type's property node via a uses/member_access edge, making ORM query sites behind DbSet<T> properties reachable. Builds property_index from defines edges restricted to .cs targets (so bare-name fallback can't reach a same-named C++ member), generalizes the method lookup into _member_on_type_or_bases, and skips parking for accesses since only cross-repo call candidates get parked. Memoizes per-caller type-name resolution and records invocation callee ids in csharp_callee_ids to distinguish call receivers from property reads without re-walking the tree.

Worth a look

  • caller_nid may be undefined for member-access branch — graphify/extractors/engine.py:6115 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2199 functions depend on the 480 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • …and 38 more — each is listed as a finding

Verification — 2199 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit b9cd957 (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2024 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

109 of 276 test file(s) selected (39%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_access.py — impact, changed-test
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_inferred_confidence_rubric.py — impact
  • tests/test_inherited_field_receivers.py — impact
  • tests/test_issue_3405_python_resolution.py — impact
  • tests/test_java_member_calls.py — impact
  • … and 59 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_resolve\_csharp\_member\_calls.

The verifier did not have enough to check \_resolve\_csharp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 46 more finding(s) on lines outside this diff (see the check run).

@Ayushraj06-bit
Ayushraj06-bit force-pushed the fix/csharp-member-access-3528 branch from fd5b241 to 5f090fa Compare September 19, 2026 17:28

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. 2 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_id\_prefixes behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'héllo\_wörld'\} but the new code produces \{'h\_llo\_w\_rld'\}. Paste that input straight into a regression test.

Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search', 'timeout': 10\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'grap… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.


Graphify review — findings

Adds C# member-access resolution to the graph: raw accesses stamped is_member_access (db.Users, this.Count) are typed by the same receiver tiers as calls and bound to the receiver type's property node via a uses/member_access edge, making ORM query sites behind DbSet<T> properties reachable even though the table name never appears in source. Generalizes the resolver so _member_on_type_or_bases walks the inherits chain against either method_index or a new property_index (restricted to .cs-declared defines targets so a bare-name receiver can't hit a same-named C++ member), and never parks accesses since parked entries are cross-repo call candidates only. Adds _csharp_member_receiver to extract simple receivers (identifier, this/base, this.field) while bailing on complex chains, memoizes type-name resolution per caller, and tracks invocation callee node ids to distinguish a call's callee member-access from a plain property read without re-walking the tree.

No blocking issues surfaced. 7 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2199 functions depend on the 480 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • …and 38 more — each is listed as a finding

Verification — 2199 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2024 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

109 of 276 test file(s) selected (39%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_access.py — impact, changed-test
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_inferred_confidence_rubric.py — impact
  • tests/test_inherited_field_receivers.py — impact
  • tests/test_issue_3405_python_resolution.py — impact
  • tests/test_java_member_calls.py — impact
  • … and 59 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_id\_prefixes behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'héllo\_wörld'\} but the new code produces \{'h\_llo\_w\_rld'\}. Paste that input straight into a regression test.

Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search', 'timeout': 10\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'grap… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_merge.

The verifier did not have enough to check build\_merge, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify prefix\_graph\_for\_global.

The verifier did not have enough to check prefix\_graph\_for\_global, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous

Could not verify: Could not verify save\_semantic\_cache.

The verifier did not have enough to check save\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in cohesion\_score (not a proof).

The verifier ran both versions of cohesion\_score on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in link\_cross\_repo\_member\_calls (not a proof).

The verifier ran both versions of link\_cross\_repo\_member\_calls on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_is\_prose\_note.

The verifier did not have enough to check \_is\_prose\_note, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify diagnose\_extraction.

The verifier did not have enough to check diagnose\_extraction, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify format\_diagnostic\_report.

The verifier did not have enough to check format\_diagnostic\_report, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_resolve\_cpp\_member\_calls.

The verifier did not have enough to check \_resolve\_cpp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_resolve\_csharp\_member\_calls.

The verifier did not have enough to check \_resolve\_csharp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify extract.

The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_php.

The verifier did not have enough to check extract\_php, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_elixir.

The verifier did not have enough to check extract\_elixir, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_js\_extra\_walk.

The verifier did not have enough to check \_js\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_ruby\_extra\_walk.

The verifier did not have enough to check \_ruby\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: signature changed: no shared positional arity

Could not verify: Could not verify extract\_markdown.

The verifier did not have enough to check extract\_markdown, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_resolve\_js\_import\_target (not a proof).

The verifier ran both versions of \_resolve\_js\_import\_target on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify extract\_rust.

The verifier did not have enough to check extract\_rust, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_terraform.

The verifier did not have enough to check extract\_terraform, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify generate.

The verifier did not have enough to check generate, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ValueError — names the real obstacle, not a sampling gap)

· 46 more finding(s) on lines outside this diff (see the check run).

…raphify-Labs#3528)

`db.Users` inside `db.Users.Where(...)` was dropped: the invocation branch
keeps only a simple receiver, so every ORM query site behind a DbSet<T>
property reached _resolve_csharp_member_calls with receiver=None and the
property node (Graphify-Labs#3006) had no inbound edge but its own type's `defines`.

Record a non-call member_access_expression on a simple receiver (recv /
this / base / this.field / Type) as a raw_calls entry stamped
is_member_access, typed from the same scoped receiver table the member
calls use, and bind it in _resolve_csharp_member_calls through the same
this/base/Type/typed tiers to a property_index built from `defines`
edges to .cs nodes — a `uses` edge at the access line, EXTRACTED when the
type is named in source and INFERRED when the receiver is typed. Never
parked: parked entries are cross-repo call candidates (Graphify-Labs#3152).

The four receiver-capture arms of the invocation branch become
_csharp_member_receiver, shared by both sites.
…hangelog hunk (Graphify-Labs#3528)

Measured on jellyfin/jellyfin (2,208 .cs files, +26.6k access entries on top
of 85.6k member calls, +6,347 `uses` edges), graph output byte-identical
before and after:

- engine.py: the member-access branch asked tree-sitter for `node.parent`
  twice per member_access_expression to tell a callee (`db.Users.Where`)
  from a property read (`db.Users`); `Node.parent` re-descends from the
  root on every call (~700ms over the corpus's 137k such nodes vs ~13ms
  for a set lookup). The walk is pre-order, so the invocation branch now
  records the callee node's id in a per-file `csharp_callee_ids` set and
  the access branch tests membership instead. Per-file extraction goes
  from +1.2-1.6s over base to +0.7-0.8s.

- extract.py: `_resolve_type_name_nid` memoizes per (type_name, caller,
  file) — a method reads and calls the same receiver many times over, and
  every access is one more entry that types it (80k -> 40k scope-chain
  walks on the corpus). The `defines` scan that builds property_index is
  folded into the existing `method` scan: one pass over all_edges fills
  both twin indexes. `_resolve_csharp_member_calls` goes from 430ms to
  288ms (base: 250ms).

- CHANGELOG.md: the entry was written under `## 0.9.61 (unreleased)`; after
  rebasing onto v8 it lands inside the released 0.9.61 section. Release
  notes here are written at the version bump (see 9335cee), so the hunk is
  dropped rather than moved.
@Ayushraj06-bit
Ayushraj06-bit force-pushed the fix/csharp-member-access-3528 branch from 5f090fa to 35d947a Compare October 5, 2026 18:54
@Ayushraj06-bit

Copy link
Copy Markdown
Contributor Author

Rebased onto current v8 (b57cf91). The branch was 366 commits behind and conflicted in engine.py.

  • Conflict: [Bug]: C#: calls through a cast, an interface-typed property, ?., or a partial class's other file are dropped without being listed as unresolved #3797 (null-conditional member calls) replaced the invocation branch's receiver capture with _csharp_member_call_parts. I kept that and left this PR's _csharp_member_receiver alongside it. The receiver logic v8 now inlines is exactly what _csharp_member_receiver does, so the invocation branch calls the helper as before. The callee id goes into csharp_callee_ids only when the callee is a plain member_access_expression. For recv?.M() the callee is a conditional_access_expression, so a receiver like db.Users in db.Users?.Where(...) is still recorded as a property read.
  • Stale test value: test_edge_carries_the_query_line_not_the_declaration_line asserted confidence_score == 0.8. Member-access edges share the receiver-typed member-call emitter, which now scores INFERRED as 0.85 on the AST "uses" edges always get INFERRED confidence_score=0.5, violating the documented rubric #2813 rubric, so the assertion now reads 0.85. It's corrected in the first commit rather than in a fixup.
  • Review finding "caller_nid may be undefined for member-access branch" (engine.py:6115 at the time): not a bug. The branch sits inside walk_calls, where caller_nid is a parameter, with no nested function in between. The resolver tests that assert the edge's source node exercise it.

Verified after the rebase (Windows 11, Python 3.11, uv sync --frozen):

uv run pytest tests/test_csharp*.py tests/test_cross_language_call_resolution.py   192 passed
uv run pytest tests/ -q   (two halves)
  b57cf91 (v8):  70 failed, 6307 passed, 146 skipped
  this branch:   70 failed, 6327 passed, 146 skipped
  (the 70 failing test ids are identical on both and already fail on a clean
   Windows checkout; none are C# tests. The +20 are this PR's tests.)
uv run ruff check .                                     All checks passed!
python -m tools.skillgen --check / --audit-coverage / --schema-singleton /
  --monolith-roundtrip / --always-on-roundtrip           all ok

pyright reports one more error than v8. It's the same receiver_types typing gap that the existing member-call site already has on v8 (_csharp_scoped_receiver_type is called with the walk's dict | tuple | None parameter). The new access branch makes the identical call, so I left both sites matching.

@Ayushraj06-bit

Copy link
Copy Markdown
Contributor Author

One last note on the review of 5f090fa. The formal-verification "behavior changes" it reported in _id_prefixes (graphify/dedup.py) and _claude_pretooluse_hooks (graphify/install.py) came from the branch's old base: the verifier was comparing against a v8 that had moved on. Since the rebase, this PR's diff touches only graphify/extract.py, graphify/extractors/engine.py and tests/test_csharp_member_access.py, so neither function is changed here. CI is green on 3.10/3.12/3.13/3.14, and the branch still merges cleanly into current v8.

@Ayushraj06-bit

Copy link
Copy Markdown
Contributor Author

@safishamsi this is rebased, green on CI and mergeable. Before you spend review time on it, one design question: do you want typed C# property reads (db.Users, this.Count) to become uses / member_access edges to the property node? That's the core of the change, and it's what makes EF Core query sites reachable from their DbSet (#3528).

If that's the right direction but you'd rather land less at once, I can split it:

  1. resolve only this.Prop / Type.Prop / field-typed receivers (the EXTRACTED tier plus the field table), with the tests for those;
  2. the perf commit as a separate PR.

Happy to go whichever way you prefer, or to close it if this isn't something you want in the graph.

This branch has not been deployed

No deployments
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.

ORM query call-sites are invisible: the table name is never in the source, so no SQL-literal fix can link them (EF Core / C#)

1 participant