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.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 — 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
- 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.
- 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.
- 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
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.CompileTimeValidate—Patterns/Aggregation/PostSharp.Patterns.Aggregation/Model/AggregatableAttribute.csThreadAwareAttribute.CompileTimeValidate—Patterns/Threading/PostSharp.Patterns.Threading/ThreadAwareAttribute.csAnnotations 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
GetAutomaticPropertyreturnsnull,memberis the field, and the field-targeted form works — which is whatCOM003tells the user to do for a non-automatic property.Observed behaviour
[Child]under a threading model[Parent]under a threading model[Reference]under a threading model[Reference]under[Aggregatable]/[Recordable]COM002[Reference]on a string, delegate or value type memberCOM004[ExplicitlySynchronized]under[Synchronized]THR020The last row, reduced:
Why this is a defect rather than a deliberate distinction
PostSharp.Patterns.Commonalready exposes the correct helper,ReflectionHelpers.IsDefinedOnFieldOrProperty, which checks the field and its automatic property.RecordableAttribute.SelectFieldsuses it for[NotRecorded], so that annotation accepts either spelling. The two analyses above hand-roll the lookup instead.ExplicitlySynchronizedAttributestates 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.MemberRelationshipAttributeis declaredAttributeTargets.Field | AttributeTargets.Property, and itsCompileTimeValidateaccepts aFieldInfotarget 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,Recordableundo/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 thatTypeAnalyzer.AreAllFieldsImmutablereads (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
ReflectionHelpers.IsDefinedOnFieldOrPropertyor a newGetCustomAttributesOnFieldOrPropertysibling that returns the instances rather than a boolean, and reportCOM001if the same member is annotated on both declarations.[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.COM004so 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