Skip to content

Latest commit

 

History

History
169 lines (127 loc) · 14.3 KB

CLAUDE.md

File metadata and controls

169 lines (127 loc) · 14.3 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What This Project Is

ExpressiveSharp is a Roslyn source generator that enables modern C# syntax (null-conditional ?., switch expressions, pattern matching) in LINQ expression trees, which normally only support a restricted subset of C#. It works by generating Expression<TDelegate> factory code at compile time from [Expressive]-decorated members.

Build & Test Commands

dotnet restore
dotnet build --no-restore -c Release
dotnet test --no-build -c Release

# Run a single test project
dotnet test tests/ExpressiveSharp.Generator.Tests -c Release

# Run a single test by name
dotnet test --filter "FullyQualifiedName~MyTestName" -c Release

# Run tests against a specific target framework
dotnet test -f net8.0 -c Release

# Accept new Verify snapshots (for snapshot tests)
VERIFY_AUTO_APPROVE=true dotnet test tests/ExpressiveSharp.Generator.Tests

# Pack NuGet packages locally
dotnet pack -c Release

# Run benchmarks (all)
dotnet run -c Release --project benchmarks/ExpressiveSharp.Benchmarks/ExpressiveSharp.Benchmarks.csproj -- --filter "*"

# Run benchmarks (specific class)
dotnet run -c Release --project benchmarks/ExpressiveSharp.Benchmarks/ExpressiveSharp.Benchmarks.csproj -- --filter "*GeneratorBenchmarks*"

CI targets the .NET 8.0, 9.0, 10.0, and 11.0 (RC) SDKs.

Architecture

Two-Phase Design

  1. Compile-time (source generation): Two incremental generators in ExpressiveSharp.Generator (targets netstandard2.0):

    • ExpressiveGenerator — finds [Expressive] members, validates them via ExpressiveInterpreter, emits expression trees via ExpressionTreeEmitter, and builds a runtime registry via ExpressionRegistryEmitter
    • PolyfillInterceptorGenerator — uses C# 13 [InterceptsLocation] to rewrite ExpressionPolyfill.Create() and IExpressiveQueryable<T> LINQ call sites from delegate form to expression tree form. Supports all standard Queryable methods, multi-lambda methods (Join, GroupJoin, GroupBy overloads), non-lambda-first methods (Zip, ExceptBy, etc.), and custom target types via [PolyfillTarget] (e.g., EF Core's EntityFrameworkQueryableExtensions for async methods)
  2. Runtime: ExpressiveResolver looks up generated expressions by (DeclaringType, MemberName, ParameterTypes). ExpressiveReplacer is an ExpressionVisitor that substitutes [Expressive] member accesses with the generated expression trees. For virtual/override [Expressive] members it dispatches polymorphically: it discovers derived [Expressive] overrides across loaded assemblies (cached, refreshed on assembly load) and emits a runtime type-test chain instance is Derived ? <derived body> : <base body> (EF Core translates this to a TPH discriminator CASE). Opt out globally via ExpressiveOptions.DisablePolymorphicDispatch() (EF: UseExpressives(o => o.DisablePolymorphicDispatch())); per-override [NotExpressive] opts a single type out. Transformers (in Transformers/) post-process trees for provider compatibility.

Key Source Files

  • src/ExpressiveSharp.Generator/ExpressiveGenerator.cs — main generator entry point
  • src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs — maps IOperation nodes to Expression.* factory calls (the heart of code generation). Uses varPrefix to ensure unique local variable names across multi-lambda emitters
  • src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.cs — validates and prepares [Expressive] members
  • src/ExpressiveSharp.Generator/PolyfillInterceptorGenerator.cs — interceptor generation. Dedicated emitters for complex methods (Join, GroupJoin, GroupBy multi-lambda), enhanced generic fallback (EmitGenericSingleLambda) for single-lambda methods with non-lambda arg forwarding, [PolyfillTarget] support for custom target types. Emits each stub's declared receiver and return types, so stubs on derived markers such as IOrderedExpressiveQueryable<T> work
  • src/ExpressiveSharp/Services/ExpressiveResolver.cs — runtime expression registry lookup
  • src/ExpressiveSharp/PolyfillTargetAttribute.cs — specifies a non-Queryable target type for interceptor forwarding (e.g., EntityFrameworkQueryableExtensions)
  • src/ExpressiveSharp/Extensions/ExpressiveQueryableLinqExtensions.cs — delegate-based LINQ stubs on IExpressiveQueryable<T> (~85 intercepted + ~15 passthrough methods)
  • src/ExpressiveSharp/IOrderedExpressiveQueryable.cs — zero-member marker bridging IOrderedQueryable<T> and IExpressiveQueryable<T>, returned by OrderBy and required by ThenBy (matching System.Linq.Queryable). Deliberately no ordered × includable cross-product: Include/AsNoTracking must precede the ordering. AsExpressive has an IOrderedQueryable<T> overload so ordering interceptors re-wrap with no generator-side factory selection
  • src/ExpressiveSharp.EntityFrameworkCore/Extensions/ExpressiveQueryableEfCoreExtensions.cs — EF Core-specific stubs: chain-continuity (AsNoTracking, TagWith, etc.), Include/ThenInclude, and async lambda methods (AnyAsync, SumAsync, etc.)
  • src/ExpressiveSharp.EntityFrameworkCore/IIncludableExpressiveQueryable.cs — hybrid interface bridging IIncludableQueryable and IExpressiveQueryable for Include/ThenInclude chain continuity
  • src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions/WindowFunction.cs — public marker methods for all SQL window functions (ranking, aggregate, navigation). All throw at runtime; translated to SQL by the method call translator
  • src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions/WindowDefinition.cs — fluent builder types: PartitionedWindowDefinition → OrderedWindowDefinition → FramedWindowDefinition. Type-safe chain ensures ORDER BY before ranking, frame only on aggregates/value functions
  • src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions/WindowFrameBound.cs — frame boundary markers: UnboundedPreceding (property), Preceding(n) (method), CurrentRow (property), Following(n) (method), UnboundedFollowing (property)
  • src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Infrastructure/Internal/WindowFunctionMethodCallTranslator.cs — translates WindowFunction.* calls to WindowFunctionSqlExpression or RowNumberExpression. Ranking functions never emit frames; aggregates propagate frames; navigation functions pass through 1–3 args
  • src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Infrastructure/Internal/WindowFunctionSqlExpression.cs — self-rendering SQL expression: FUNC(args) OVER(PARTITION BY ... ORDER BY ... [ROWS/RANGE BETWEEN ...]). Provider-agnostic via SQL:2003 standard syntax

Project Dependencies

ExpressiveSharp.Abstractions (attributes + source generator, net8.0;net9.0;net10.0;net11.0)
  └── no external deps
  Provides: [Expressive], [ExpressiveFor], [ExpressiveForConstructor], [PolyfillTarget],
      IExpressionTreeTransformer, source generator + code fixers (as analyzers)

ExpressiveSharp (core runtime, net8.0;net9.0;net10.0;net11.0)
  └── ExpressiveSharp.Abstractions
  Provides: ExpressiveResolver, ExpressiveReplacer, expression transformers,
      IExpressiveQueryable<T>, ExpressionPolyfill, .ExpandExpressives(), .AsExpressive()

ExpressiveSharp.Generator (source generator, netstandard2.0)
  └── Microsoft.CodeAnalysis.CSharp 5.0.0

ExpressiveSharp.MongoDB (net8.0;net9.0;net10.0;net11.0)
  ├── ExpressiveSharp
  ├── MongoDB.Driver 3.4.0
  └── Provides: IRewritableMongoQueryable<T>, ExpressiveMongoCollection<T>,
      ExpressiveMongoQueryProvider (decorating IQueryProvider/IMongoQueryProvider),
      async lambda stubs with [PolyfillTarget(typeof(MongoQueryable))]

ExpressiveSharp.EntityFrameworkCore (net8.0;net9.0;net10.0;net11.0)
  ├── ExpressiveSharp
  ├── EF Core 8.0.25 / 9.0.0 / 10.0.0 / 11.0.0-rc.1 (per-TFM VersionOverride)
  └── Provides: ExpressiveDbSet<T>, IIncludableExpressiveQueryable<T,P>,
      chain-continuity stubs, async lambda stubs with [PolyfillTarget]

ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions (net8.0;net9.0;net10.0 — no net11.0, see below)
  └── no external deps
  Provides: Pure marker types for window functions — no EF Core dependency:
      WindowFunction (ranking: ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST;
          aggregate: SUM, AVG, COUNT, MIN, MAX; navigation: LAG, LEAD, FIRST_VALUE,
          LAST_VALUE, NTH_VALUE), Window, OrderedWindowDefinition,
          PartitionedWindowDefinition, FramedWindowDefinition, WindowFrameBound

ExpressiveSharp.EntityFrameworkCore.RelationalExtensions (net8.0;net9.0;net10.0 — no net11.0, see below)
  ├── ExpressiveSharp.EntityFrameworkCore
  ├── ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions
  ├── EF Core Relational 8.0.25 / 9.0.0 / 10.0.0 (per-TFM VersionOverride)
  └── Provides: Window function SQL translation (ranking, aggregate with ROWS/RANGE
      frame clauses, navigation with LAG/LEAD/FIRST_VALUE/LAST_VALUE/NTH_VALUE),
      indexed Select, activated via UseExpressives(o => o.UseRelationalExtensions()).
      Translators: WindowFunctionMethodCallTranslator, WindowSpecMethodCallTranslator,
      WindowFrameBoundMemberTranslator (IMemberTranslatorPlugin for property getters).
      Note: NTH_VALUE is not supported on SQL Server.
      EF Core 11 is unsupported: both RelationalExtensions packages deliberately do
      not target net11.0 — EF 11 removed the QuerySqlGenerator VisitChildren fallback
      that WindowFunctionSqlExpression's self-rendering relied on, with no replacement
      extension point (dotnet/efcore#37533; tracked in dotnet/efcore#38977). Test
      projects gate on a RelationalExtensionsSupported msbuild property. Add the TFM
      back (and drop those gates) once EF provides a hook.

ExpressiveSharp.EntityFrameworkCore.CodeFixers (Roslyn analyzer, netstandard2.0)
  └── Microsoft.CodeAnalysis.CSharp.Workspaces 4.12.0
  (packed into ExpressiveSharp.EntityFrameworkCore NuGet package)

Diagnostics

Diagnostic codes span EXP0001–EXP0032 (with EXP0024 retired — see below — so the ID is reserved but unused) plus the migration band EXP1001–EXP1003. Generator diagnostics live in src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs (EXP0001–EXP0012 core [Expressive], EXP0013–EXP0017 [ExpressiveFor], EXP0018–EXP0022 [ExpressiveProperty], EXP0023 ignored operation); analyzer diagnostics EXP0025–EXP0029 and EXP0032 in ExpressiveSharp.CodeFixers; window-function diagnostics EXP0030/EXP0031 in WindowFunctionLiteralArgsAnalyzer (EntityFrameworkCore.CodeFixers); migration EXP1001–EXP1003 in MigrationAnalyzer. The canonical reference is docs/reference/diagnostics.md. Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0023 (unsupported operation ignored, e.g. alignment specifiers), EXP0016 ([ExpressiveFor] conflicts with [Expressive]), EXP0030 (Ntile non-positive literal), EXP0031 (Lag/Lead negative literal offset), EXP0032 (override of an [Expressive] member missing [Expressive]). EXP0024 (retired): virtual/abstract/override [Expressive] members used to warn that they would not dispatch polymorphically; they now do dispatch polymorphically at runtime (ExpressiveReplacer emits an is Derived ? … : base chain, translated to a TPH discriminator CASE), so the warning was removed and EXP0032 took over the derived-side guidance.

Testing

Test Projects

Project Purpose
ExpressiveSharp.Generator.Tests Snapshot tests (Verify.MSTest) — validates generated C# output
ExpressiveSharp.Tests Unit tests for runtime services, transformers, extensions
ExpressiveSharp.IntegrationTests Compiles expression trees to delegates and runs them against in-memory collections — provider-agnostic correctness for every feature (arithmetic, switch, pattern matching, null-conditional, loops, tuples, [Expressive] expansion). Also hosts the shared Store scenario models + seed data.
ExpressiveSharp.EntityFrameworkCore.IntegrationTests All EF Core integration tests — scenarios, async queryables, window functions, ExecuteUpdate, Include, query filters, conventions. Runs against SQLite by default; containerized SqlServer/Postgres/PomeloMySql/Cosmos via -p:TestDatabase=<provider>
ExpressiveSharp.Benchmarks BenchmarkDotNet performance benchmarks (generator, resolver, replacer, transformers, EF Core)

Three Verification Levels (see docs/testing-strategy.md)

  1. Compilation — generated code compiles without errors
  2. Diagnostics — no warnings (TreatWarningsAsErrors is enabled globally)
  3. Behavioral — expression tree is functionally equivalent to the original code

Snapshot tests use GeneratorTestBase.RunExpressiveGenerator() to compile via Roslyn and compare output.

Code Conventions

  • TreatWarningsAsErrors: true — all warnings are errors
  • Nullable: enable — full nullable reference types
  • C# 14.0 on all TFMs (net8.0–net11.0; LangVersion is set globally in Directory.Build.props)
  • Allman brace style, 4-space indentation, var preferred
  • Instance fields: _camelCase
  • Expression-bodied members preferred for methods/properties

Gotchas

  • Generator targets netstandard2.0 — no modern C# APIs (no spans, no Index/Range, limited BCL). All generator code must compile against netstandard2.0 even though the rest of the solution uses C# 12+/14.
  • Snapshot tests — *.verified.cs files alongside tests in tests/ExpressiveSharp.Generator.Tests/ are the expected output. Commit them. Use VERIFY_AUTO_APPROVE=true to accept new baselines.
  • Incremental generator caching — never store live Roslyn objects (symbols, syntax nodes) in cached pipeline state. Use immutable data snapshots with custom equality comparers.
  • Central Package Management — all package versions are pinned in Directory.Packages.props. When adding a new dependency, add its version there (not in the individual .csproj).

MSBuild Properties

  • Expressive_AllowBlockBody — global default for block-bodied [Expressive] members (defaults to false; can also be set per-member via attribute)