Skip to content

Relationship attributes and [ExplicitlySynchronized] applied with field: to an automatic property are silently ignored #112

Description

Summary

A relationship attribute ([Child], [Parent], [Reference]) or [ExplicitlySynchronized] can be applied to the backing field of an automatic property with the C# field: target specifier. In this case, the attribute 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 do not take effect.

This behaviour was verified against the packages published on nuget.org, version 2026.0.16.

Root cause

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

  • AggregatableAttribute.CompileTimeValidate — Patterns/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.CompileTimeValidate — Patterns/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. For a non-automatic property, COM003 tells the user to use this form.

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 following reduced example reproduces the last row:

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. This helper checks both the field and its automatic property. RecordableAttribute.SelectFields uses it for [NotRecorded], so that annotation accepts either form. The two analyses above implement the lookup themselves instead of using this helper.
  • 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 therefore documented as supported. It is supported, except on an automatic property.
  • MemberRelationshipAttribute is declared with AttributeTargets.Field | AttributeTargets.Property. Its CompileTimeValidate accepts a FieldInfo target without reporting an error. The annotation is therefore validated and then ignored.

The same change should also resolve an inconsistency. 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. In the field-targeted form, it is silently accepted.

Impact

[field: Child] and [field: Parent] on an automatic property are silent correctness defects. The object graph is not built as written. Behaviour that depends on aggregation (child threading model propagation, Recordable undo/redo, Disposable) does not apply to those members. Nothing reports the problem.

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

Proposed fix

  1. At both sites, read the annotations from the field and from its automatic property. Use ReflectionHelpers.IsDefinedOnFieldOrProperty, or a new sibling method GetCustomAttributesOnFieldOrProperty that returns the attribute instances instead of a boolean. 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 the backing field of an automatic property and is about to be discarded. A warning would make every existing occurrence visible without changing behaviour. It 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 form.

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

Reproduction

A self-contained xunit project that reproduces all of the above against the published 2026.0.16 packages is available on request. It does not need the 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