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
- 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.
- 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.
- 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
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.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. For a non-automatic property,COM003tells the user to use this form.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 following reduced example reproduces the last row:
Why this is a defect rather than a deliberate distinction
PostSharp.Patterns.Commonalready exposes the correct helper,ReflectionHelpers.IsDefinedOnFieldOrProperty. This helper checks both the field and its automatic property.RecordableAttribute.SelectFieldsuses it for[NotRecorded], so that annotation accepts either form. The two analyses above implement the lookup themselves instead of using this helper.ExplicitlySynchronizedAttributestates 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.MemberRelationshipAttributeis declared withAttributeTargets.Field | AttributeTargets.Property. ItsCompileTimeValidateaccepts aFieldInfotarget 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,Recordableundo/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.AreAllFieldsImmutablereads 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
ReflectionHelpers.IsDefinedOnFieldOrProperty, or a new sibling methodGetCustomAttributesOnFieldOrPropertythat returns the attribute instances instead of a boolean. ReportCOM001if the same member is annotated on both declarations.[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.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