Skip to content

[ExpressiveProperty] Attribute

[ExpressiveProperty] synthesizes a settable property on the decorated stub's containing partial type. The stub supplies the formula; the generator emits the target property with a backing field and an init accessor, so projection middleware (HotChocolate [UseProjection], AutoMapper ProjectTo, Mapperly, EF Core materialization) can bind to it. In-memory reads fall through to the stub until a value is materialized; after materialization, the stored value wins.

Namespace

csharp
using ExpressiveSharp.Mapping;

Minimum example

csharp
public partial class Account
{
    public decimal? TotalAmount { get; set; }
    public decimal? Discount    { get; set; }

    // Amount is NOT declared here — the generator emits it.
    [ExpressiveProperty("Amount")]
    private decimal? AmountExpression =>
        TotalAmount != null && Discount != null
            ? TotalAmount.Value - Discount.Value
            : null;
}

The generator emits, alongside the expression-tree factory:

csharp
// <auto-generated/>
namespace YourNamespace
{
    partial class Account
    {
        private decimal? _amount;
        private bool _amountHasValue;
        public decimal? Amount
        {
            get => _amountHasValue ? _amount : AmountExpression;
            init
            {
                _amountHasValue = true;
                _amount = value;
            }
        }
    }
}

Rules

  1. Placement. Only valid on a property declaration with a top-level expression body (=> expr). Method stubs and accessor-list forms ({ get => expr; }) are rejected. This keeps the feature's surface small and unambiguous.
  2. Instance only. Static stubs are rejected. If you need a static computed value, use plain [Expressive] on a read-only member.
  3. Explicit target name. Pass the name as a string literal: [ExpressiveProperty("Amount")]. nameof(Amount) fails to resolve because Amount does not yet exist during the generator's pass.
  4. Partial type. The containing class, struct, or record must be declared partial so the generator can emit into it.
  5. Unique name. The target name must not already exist on the containing type or any of its base types. Use plain [ExpressiveFor(nameof(X))] to map onto an existing member instead.
  6. One target per stub. AllowMultiple = false; the attribute cannot be stacked to alias one stub to multiple targets.

Shape selection

The generator picks between two shapes based on the target type's nullability:

Target typeShapeBacking fieldFlag field
Non-nullable ref (string)Coalescestring?
Non-nullable value (decimal)CoalesceNullable<decimal>
Nullable ref (string?)Ternarystring?bool
Nullable value (decimal?)Ternarydecimal?bool

Coalesce uses get => _x ?? stub. Ternary uses get => _xHasValue ? _x : stub. The ternary shape is necessary whenever null is a legitimate stored value — otherwise the cache sentinel and the materialized value collide.

Visibility

The synthesized property is always public with an init accessor. Projection middleware needs the property to be externally reachable and writable-once — public + init satisfies both with no opt-in surface.

If you need an internal synthesized property, cap the effective visibility by placing the whole type inside an internal partial class. For a non-materializable read-only computed value, use [Expressive] directly.

If you need a mutable set (not just init), that's a future addition; open an issue describing the scenario.

When to use [ExpressiveFor] instead

ScenarioUse
Target property already exists on your type (e.g. it also has runtime state)[ExpressiveFor(nameof(X))]
Target lives on an external type (BCL, third-party)[ExpressiveFor(typeof(T), "X")]
Target does not exist and you want a settable property for projection middleware[ExpressiveProperty("X")] (this page)
Target is a computed read-only value, no materialization needed[Expressive]

Diagnostics

CodeCause
EXP0031Target name already defined on the containing type. Rename the stub, or switch to [ExpressiveFor(nameof(X))].
EXP0032Containing type is not partial.
EXP0033Stub is not a property with top-level expression body.
EXP0034Stub is static.
EXP0035Target name shadows an inherited member on a base type.

EF Core and MongoDB integration

Synthesized properties are automatically excluded from EF Core's model (ExpressivePropertiesNotMappedConvention) and from MongoDB's BSON class map (ExpressiveMongoIgnoreConvention). You do not need [NotMapped] or [BsonIgnore]. The registry entry is keyed on the synthesized property's getter, so ExpressiveReplacer rewrites references to it into the stub formula at query time.

Released under the MIT License.