Open sandboxFocusImprove this doc

Automatically ignoring property values without boilerplate

This very sample aspect overrides the target field or property so that any attempt to set it to one of the forbidden value is simply ignored.

Source Code
1internal class Author
2{
3    [IgnoreValues("", null)] public string Name { get; set; }


4
5    public Author(string name)
6    {
7        this.Name = name;
8    }
9}
Transformed Code
1internal class Author
2{
3private string _name = default!;
4
5    [IgnoreValues("", null)] public string Name { get { return _name; } set { if (value == "") { return; } if (value == null) { return; } this._name = value; } }
6
7    public Author(string name)
8    {
9        this.Name = name;
10    }
11}

Implementation

The aspect class is derived from FieldOrPropertyAspect, which itself derives from Attribute.

The aspect constructor accepts the list of forbidden values, and stores them as a field.

7private readonly object?[] _ignoredValues; 
8
9public IgnoreValuesAttribute(params object?[] values)
10{
11    this._ignoredValues = values;
12}

The OverrideProperty property is the template overriding the original property. The getter implementation, get => meta.Proceed(), means that the getter is not modified. In the setter, we have a compile-time foreach loop that, for each forbidden value, tests if the assigned value is equal to this forbidden value and, if it is the case, returns before calling meta.Proceed(), i.e. before assigning the underlying field.

14public override dynamic? OverrideProperty
15{
16    get => meta.Proceed();
17    set
18    {
19        foreach (var ignoredValue in this._ignoredValues)
20        {
21            if (value == meta.RunTime(ignoredValue))
22            {
23                return;
24            }
25        }
26
27        meta.Proceed();
28    }
29}
30

This simple approach works well for most types you can use in an attribute constructor, but not for all of them:

  • For enums (except .NET Standard 2.0 enums), the constructor will receive the underlying integer value instead of a typed value. This means that our comparison will generate invalid C# because it will compare an enum to an integer.
  • For arrays, a simple == comparison is not sufficient.

Both cases could be handled by a more complex aspect. However, in this example, we will simply prevent the aspect from being applied to fields or properties of an unsupported type. We achieve this by implementing the BuildEligibility method.

31public override void BuildEligibility(IEligibilityBuilder<IFieldOrProperty> builder)
32{
33    var supportedTypes =
34        new[]
35        {
36            typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float),
37            typeof(double), typeof(decimal), typeof(short), typeof(sbyte), typeof(byte),
38            typeof(ushort), typeof(char), typeof(string), typeof(bool), typeof(Type)
39        };
40
41    builder.Type().MustSatisfyAny(supportedTypes.Select(supportedType =>
42        new Action<IEligibilityBuilder<IType>>(t => t.MustBe(supportedType))).ToArray());
43}

Complete source code

Here is the complete source code of the aspect.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Eligibility;
4
5internal class IgnoreValuesAttribute : OverrideFieldOrPropertyAspect
6{
7    private readonly object?[] _ignoredValues; 
8
9    public IgnoreValuesAttribute(params object?[] values)
10    {
11        this._ignoredValues = values;
12    }
13
14    public override dynamic? OverrideProperty
15    {
16        get => meta.Proceed();
17        set
18        {
19            foreach (var ignoredValue in this._ignoredValues)
20            {
21                if (value == meta.RunTime(ignoredValue))
22                {
23                    return;
24                }
25            }
26
27            meta.Proceed();
28        }
29    }
30
31    public override void BuildEligibility(IEligibilityBuilder<IFieldOrProperty> builder)
32    {
33        var supportedTypes =
34            new[]
35            {
36                typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float),
37                typeof(double), typeof(decimal), typeof(short), typeof(sbyte), typeof(byte),
38                typeof(ushort), typeof(char), typeof(string), typeof(bool), typeof(Type)
39            };
40
41        builder.Type().MustSatisfyAny(supportedTypes.Select(supportedType =>
42            new Action<IEligibilityBuilder<IType>>(t => t.MustBe(supportedType))).ToArray());
43    }
44}