Skip to content

feat: add [Expressive(Projectable = true)] for projection middleware compatibility#31

Merged
koenbeuk merged 3 commits intomainfrom
feat/expressive-projectables
Apr 13, 2026
Merged

feat: add [Expressive(Projectable = true)] for projection middleware compatibility#31
koenbeuk merged 3 commits intomainfrom
feat/expressive-projectables

Conversation

@koenbeuk
Copy link
Copy Markdown
Collaborator

Allows a special form of Expressive members, projectables! (No relation to the original project).

Problem: Expressive properties silently return garbage when consumed by projection middleware (HotChocolate, AutoMapper ProjectTo, Mapperly). The middleware drops read-only members from its Select(src => new Entity { ... }) binding because they fail a CanWrite check, so the query fetches nothing and the getter runs against empty defaults.

Solution: [Expressive(Projectable = true)] opts the property into a writable field ?? () shape. The middleware sees a writable target and emits the binding; ExpressiveSharp extracts the formula and pushes it into SQL; the result materializes through init.

Copilot AI review requested due to automatic review settings April 13, 2026 01:11
@koenbeuk
Copy link
Copy Markdown
Collaborator Author

Example usecase:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; } = "";
    public string LastName  { get; set; } = "";

    [Expressive(Projectable = true)]
    public string FullName
    {
        get => field ?? (LastName + ", " + FirstName);
        init => field = value;
    }
}

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces Projectable properties via [Expressive(Projectable = true)] to ensure computed members participate correctly in “project-into-same-type” projection middleware scenarios (e.g., HotChocolate projections / AutoMapper ProjectTo), while still allowing ExpressiveSharp to translate the underlying formula into provider SQL/query expressions.

Changes:

  • Added a new [Expressive(Projectable = true)] mode with generator support, pattern recognition, and new diagnostics (EXP0021–EXP0029).
  • Added runtime/provider integration coverage via new EF Core SQLite + MongoDB integration tests and resolver tests ensuring registry keying and rewrite correctness.
  • Added end-user documentation and site navigation entries for Projectable properties, projection middleware usage, and diagnostics reference.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/ExpressiveSharp.Tests/Services/ExpressiveResolverTests.cs Adds resolver-level tests ensuring Projectable expressions resolve by property getter and only capture the formula.
tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ProjectableMongoIgnoreTests.cs Adds Mongo integration coverage for ignoring Projectable properties in BSON and ensuring formula behavior survives round-trip.
tests/ExpressiveSharp.IntegrationTests/Tests/ProjectableExpressiveTests.cs Adds provider-agnostic runtime semantics tests (formula vs. stored value) and expression expansion tests.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ProjectableTests.SimpleProjectableProperty_ManualBackingField.verified.txt Verified generator output for manual nullable backing-field Projectable pattern.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ProjectableTests.SimpleProjectableProperty_FieldKeyword.verified.txt Verified generator output for C# field keyword Projectable pattern.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ProjectableTests.ProjectableWithSetAccessor.verified.txt Verified generator output for Projectable properties using set instead of init.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ProjectableTests.cs Adds generator tests for happy paths, registry-key correctness, and negative/diagnostic cases.
tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/ProjectableExpressiveSqlTests.cs Adds EF Core SQLite tests for model ignoring + SQL inlining + member-init materialization behavior.
src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoIgnoreConvention.cs Introduces a Mongo convention to unmap [Expressive]/Projectable properties from BSON mapping.
src/ExpressiveSharp.MongoDB/ExpressiveMongoCollection.cs Ensures the Mongo ignore convention is registered when using the collection wrapper entry point.
src/ExpressiveSharp.Generator/Models/ExpressiveAttributeData.cs Adds Projectable attribute argument parsing into generator model.
src/ExpressiveSharp.Generator/Interpretation/ProjectablePatternRecognizer.cs Adds IOperation-based pattern recognition for Projectable getter/setter shapes.
src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.cs Routes Projectable properties to specialized interpretation pipeline.
src/ExpressiveSharp.Generator/Interpretation/ExpressiveInterpreter.BodyProcessors.cs Implements Projectable property validation, pattern extraction, and formula emission via existing pipeline.
src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs Adds new Projectable diagnostics EXP0021–EXP0029.
src/ExpressiveSharp.Abstractions/ExpressiveAttribute.cs Adds public API surface bool Projectable { get; set; } with semantics documentation.
docs/reference/projectable-properties.md New reference page detailing motivation, semantics, syntax, and restrictions.
docs/reference/expressive-attribute.md Documents the new Projectable attribute property and links to the full reference.
docs/reference/diagnostics.md Adds Projectable diagnostics overview and per-diagnostic guidance (EXP0021–EXP0029).
docs/recipes/projection-middleware.md New recipe page for HotChocolate/AutoMapper projection middleware scenarios.
docs/guide/migration-from-projectables.md Updates migration guide to include Projectable as a UseMemberBody replacement option.
docs/.vitepress/config.mts Adds sidebar links for the new reference/recipe pages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

koenbeuk and others added 2 commits April 13, 2026 01:53
- Replace Unicode em-dashes with double-hyphens to match reference/recipe style
- Rename "Further reading" → "See Also" in the projection-middleware recipe
- Update UseMemberBody migration row to mention both replacement options
- Add MongoDB BSON unmapping convention note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@koenbeuk koenbeuk force-pushed the feat/expressive-projectables branch from 842a365 to 90c7a82 Compare April 13, 2026 01:59
@wassim-k
Copy link
Copy Markdown

Hi @koenbeuk, that was quick! And yeh, I see what you meant by "odd-ball"

Part of the rason I like this, is because (believe it or not) I wrote my own source generator on top of Projectables that took:

[Projectable(UseMemberBody = nameof(FullNameExpr))]
public partial string FullName { get; init; }

private string FullNameExpr => LastName + ", " + FirstName;

And generated exactly:

public partial string FullName
{
    get => field ?? FullNameExpr;
    init => field = value;
}

The reason we had to do that, was because we use EF Core's lazy loading proxies in CQRS Command context for updates, and we needed projectable properties to evaluate correctly when lazily accessed.

So this new approach automatically handles that for us.

While it looks a bit odd at first glance, I think it works. I also like the fact, that unlike UseMemberBody, you don't have to manually match the type of the property and its expression property (body)

Out of curiousity was there a technical reason why you chose this syntax over UseMemberBody or do you just prefer this syntax?

@wassim-k
Copy link
Copy Markdown

wassim-k commented Apr 13, 2026

Another question which may have an obvious answer. Is Projectable = true required or can the library infer it from the full body syntax automatically?

@koenbeuk
Copy link
Copy Markdown
Collaborator Author

koenbeuk commented Apr 13, 2026

Out of curiousity was there a technical reason why you chose this syntax over UseMemberBody or do you just prefer this syntax?

No technical reason. UseMemberBody was always a bit of a hack intended to work around limitations in what could be expressed. (It was never designed to provide a workaround for the projection limitation that you ran into that motivated this PR but conveniently enough it let itself be used as a workaround). The replacement of the intended use of UseMemberBody is ExpressiveFor which widens the scope as it also allows a user to make external members (members that the user didn't write or control) expressive.

This PR is an attempt to provide proper support for the scenario you ran into.

Another question which may have an obvious answer. Is Projectable = true required or can the library infer it from the full body syntax automatically?

Good point! but keeping the explicit flag preserves the intent signal at the declaration site and guards against accidental opt‑in plus a code analyzer can flag incorrect usage

- Reject static backing fields in Pattern B of ProjectablePatternRecognizer.
  A static field would share materialized state across all instances, breaking
  per-entity semantics. Adds a snapshot test for the EXP0022 diagnostic.

- Register ExpressiveMongoIgnoreConvention from the AsExpressive extension path
  (not just the ExpressiveMongoCollection<T> constructor). Document the ordering
  constraint with MongoDB's eager class-map caching, and recommend calling
  ExpressiveMongoIgnoreConvention.EnsureRegistered() at startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@koenbeuk koenbeuk merged commit 8026759 into main Apr 13, 2026
20 checks passed
@koenbeuk koenbeuk deleted the feat/expressive-projectables branch April 13, 2026 20:14
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