Skip to content

Relationship attributes and [ExplicitlySynchronized] are discarded when applied to an automatic property's backing field #112

Description

Summary

When a relationship attribute ([Child], [Parent], [Reference]) or [ExplicitlySynchronized] is applied to an automatic property's backing field with the C# field: target specifier, it is discarded. The annotation is written to the field, but the analysis reads it from the property.

Under a threading model the omission is silent: [field: Child] and [field: Parent] produce no diagnostic and simply do not take effect.

Verified against the packages published on nuget.org, version 2026.0.16.

Root cause

Two analyses enumerate fields, re-target an automatic property's backing field to the property, and then read the annotation from that one member:

  • AggregatableAttribute.CompileTimeValidatePatterns/Aggregation/PostSharp.Patterns.Aggregation/Model/AggregatableAttribute.cs

    PropertyInfo property = field.GetAutomaticProperty( false );          // line 288
    MemberInfo member = property != null ? (MemberInfo) property : field;
    ...
    IList<MemberRelationshipAttribute> attributes =
        ReflectionSearch.GetCustomAttributesOnTarget<MemberRelationshipAttribute>( member );   // line 308
  • ThreadAwareAttribute.CompileTimeValidatePatterns/Threading/PostSharp.Patterns.Threading/ThreadAwareAttribute.cs

    PropertyInfo automaticProperty = field.GetAutomaticProperty( false ); // line 332
    MemberInfo member = (MemberInfo) automaticProperty ?? field;
    if ( ReflectionSearch.HasCustomAttribute( member, typeof(ExplicitlySynchronizedAttribute) ) )  // line 335

Annotations are keyed strictly by target declaration, so a field-targeted annotation never appears when the property is queried.

Only automatic properties are affected. On an explicitly declared field GetAutomaticProperty returns null, member is the field, and the field-targeted form works — which is what COM003 tells the user to do for a non-automatic property.

Observed behaviour

Declaration Property-targeted Field-targeted
[Child] under a threading model child attached silently not attached, no diagnostic
[Parent] under a threading model parent populated silently not populated, no diagnostic
[Reference] under a threading model reference reference (legacy-mode default coincides)
[Reference] under [Aggregatable] / [Recordable] accepted COM002
[Reference] on a string, delegate or value type member COM004 accepted
[ExplicitlySynchronized] under [Synchronized] exemption honoured exemption lost, THR020

The last row, reduced:

public class NotThreadSafe { public int Value; }

[Synchronized]
public class WithPropertyTargetedExemption
{
    [Child]
    [ExplicitlySynchronized]
    public NotThreadSafe Item { get; set; }          // builds clean
}

[Synchronized]
public class WithFieldTargetedExemption
{
    [Child]
    [field: ExplicitlySynchronized]
    public NotThreadSafe Item { get; set; }          // error THR020
}
error THR020: The type of field WithFieldTargetedExemption.Item is not compatible with the
[SynchronizedAttribute] aspect: the type is not immutable and has no threading model.

Why this is a defect rather than a deliberate distinction

  • PostSharp.Patterns.Common already exposes the correct helper, ReflectionHelpers.IsDefinedOnFieldOrProperty, which checks the field and its automatic property. RecordableAttribute.SelectFields uses it for [NotRecorded], so that annotation accepts either spelling. The two analyses above hand-roll the lookup instead.
  • The XML documentation of ExplicitlySynchronizedAttribute states that "when the custom attribute is applied to a field, accesses to this field are never checked". The field-targeted form is documented as supported, and it is, except on an automatic property.
  • MemberRelationshipAttribute is declared AttributeTargets.Field | AttributeTargets.Property, and its CompileTimeValidate accepts a FieldInfo target without complaint, so the annotation is validated and then ignored.

There is also an inconsistency worth resolving in the same change: a relationship attribute on a member whose type is a string, a delegate or a value type is an error (COM004) in the property-targeted form and silently accepted in the field-targeted form.

Impact

[field: Child] and [field: Parent] on an automatic property are silent correctness defects: the object graph is not built as written, aggregation-dependent behaviour (child threading model propagation, Recordable undo/redo, Disposable) does not apply to those members, and nothing reports it.

[field: Reference] is comparatively benign, but it is not equivalent to [Reference]: it is the field-targeted form that TypeAnalyzer.AreAllFieldsImmutable reads (field.IsDefined( typeof(ReferenceAttribute), false )) when deciding whether a type with read-only fields is immutable. On a get-only or init-only automatic property the two spellings therefore classify the declaring type differently, which changes build-time thread-safety verification for every threaded class holding a field of that type. Converting [field: Reference] to [Reference] is not unconditionally safe.

Proposed fix

  1. Read the annotations from the field and its automatic property at both sites, through ReflectionHelpers.IsDefinedOnFieldOrProperty or a new GetCustomAttributesOnFieldOrProperty sibling that returns the instances rather than a boolean, and report COM001 if the same member is annotated on both declarations.
  2. Alternatively, or in addition, emit a diagnostic when a relationship attribute or [ExplicitlySynchronized] is found on an automatic property's backing field and is about to be discarded. A warning would make every existing occurrence visible without changing behaviour, and could be escalated to an error later.
  3. Align COM004 so that the field-targeted form is rejected on the same member types as the property-targeted one.

Option 1 changes behaviour for code that currently compiles, so it needs a release note: a [field: Child] that is ignored today would start attaching the child.

Reproduction

A self-contained xunit project reproducing all of the above against the published 2026.0.16 packages is available on request; it needs no PostSharp source tree.

-- Claude for Gael Fraiteur

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions