In Getting started with overriding fields and properties, you learned the basics of the OverrideFieldOrPropertyAspect class. This article covers more advanced scenarios.
Accessing metadata of the overridden field or property
The metadata of the overridden field or property can be accessed from the template accessors on the meta.Target.FieldOrProperty property. This property provides all information about the field or property's name, type, and custom attributes. For instance, the member name is available on meta.Target.FieldOrProperty.Name and its type on meta.Target.FieldOrProperty.Type.
- meta.Target.FieldOrProperty exposes the current field or property as an IFieldOrProperty, which reveals characteristics common to fields and properties.
- meta.Target.Field exposes the current field as an IField, but throws an exception if the target is not a field.
- meta.Target.Property exposes the current property as an IProperty, but throws an exception if the target is not a property.
- meta.Target.Method exposes the current accessor method. This works even if the target is a field because Metalama creates pseudo methods to represent field accessors.
To access the value of the field or property, use the meta.Target.FieldOrProperty.Value expression for both reading and writing. In the setter template, meta.Target.Parameters[0].Value gives you the value of the value parameter.
Example: Resolving dependencies on the fly
The following example is a simplified implementation of the service locator pattern.
The Import aspect overrides the getter of a property to call a global service locator. The aspect determines the service type from the field or property type using meta.Target.FieldOrProperty.Type. The dependency isn't stored, so the aspect calls the service locator every time the property is accessed.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.GlobalImport;
5
6internal class ImportAttribute : OverrideFieldOrPropertyAspect
7{
8 public override dynamic? OverrideProperty
9 {
10 get
11 => ServiceLocator.ServiceProvider.GetService(
12 meta.Target.FieldOrProperty.Type.ToType() );
13
14 set
15 => throw new NotSupportedException(
16 $"{meta.Target.FieldOrProperty.Name} should not be set from source code." );
17 }
18}
1using System;
2using System.Collections.Generic;
3
4namespace Doc.GlobalImport;
5
6internal class Foo
7{
8 [Import]
9 private IFormatProvider? FormatProvider { get; }
10}
11
12internal class ServiceLocator : IServiceProvider
13{
14 private static readonly ServiceLocator _instance = new();
15 private readonly Dictionary<Type, object> _services = new();
16
17 public static IServiceProvider ServiceProvider => _instance;
18
19 object? IServiceProvider.GetService( Type serviceType )
20 {
21 this._services.TryGetValue( serviceType, out var value );
22
23 return value;
24 }
25
26 public static void AddService<T>( T service ) where T : class
27 => _instance._services[typeof(T)] = service;
28}
1using System;
2using System.Collections.Generic;
3
4namespace Doc.GlobalImport;
5
6internal class Foo
7{
8 [Import]
9 private IFormatProvider? FormatProvider
10 {
11 get
12 {
13 return (IFormatProvider?)ServiceLocator.ServiceProvider.GetService(typeof(IFormatProvider));
14 }
15
16 init
17 {
18 throw new NotSupportedException("FormatProvider should not be set from source code.");
19 }
20 }
21}
22
23internal class ServiceLocator : IServiceProvider
24{
25 private static readonly ServiceLocator _instance = new();
26 private readonly Dictionary<Type, object> _services = new();
27
28 public static IServiceProvider ServiceProvider => _instance;
29
30 object? IServiceProvider.GetService(Type serviceType)
31 {
32 this._services.TryGetValue(serviceType, out var value);
33
34 return value;
35 }
36
37 public static void AddService<T>(T service) where T : class
38 => _instance._services[typeof(T)] = service;
39}
Example: Resolving dependencies on the fly and storing the result
This example builds on the previous one but stores the dependency in the field or property after first retrieval from the service provider.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.GlobalImportWithSetter;
5
6internal class ImportAttribute : OverrideFieldOrPropertyAspect
7{
8 public override dynamic? OverrideProperty
9 {
10 get
11 {
12 // Gets the current value of the field or property.
13 var service = meta.Proceed();
14
15 if ( service == null )
16 {
17 // Call the service provider.
18 service =
19 meta.Cast(
20 meta.Target.FieldOrProperty.Type,
21 ServiceLocator.ServiceProvider.GetService(
22 meta.Target.FieldOrProperty.Type.ToType() ) );
23
24 // Set the field or property to the new value.
25 meta.Target.FieldOrProperty.Value = service;
26 }
27
28 return service;
29 }
30
31 set => throw new NotSupportedException();
32 }
33}
1using System;
2using System.Collections.Generic;
3
4namespace Doc.GlobalImportWithSetter;
5
6internal class Foo
7{
8 [Import]
9 private IFormatProvider? _formatProvider;
10}
11
12internal class ServiceLocator : IServiceProvider
13{
14 private static readonly ServiceLocator _instance = new();
15 private readonly Dictionary<Type, object> _services = new();
16
17 public static IServiceProvider ServiceProvider => _instance;
18
19 object? IServiceProvider.GetService( Type serviceType )
20 {
21 this._services.TryGetValue( serviceType, out var value );
22
23 return value;
24 }
25
26 public static void AddService<T>( T service ) where T : class
27 => _instance._services[typeof(T)] = service;
28}
1using System;
2using System.Collections.Generic;
3
4namespace Doc.GlobalImportWithSetter;
5
6internal class Foo
7{
8 private IFormatProvider? _formatProvider1;
9
10 [Import]
11 private IFormatProvider? _formatProvider
12 {
13 get
14 {
15 var service = _formatProvider1;
16 if (service == null)
17 {
18 service = (IFormatProvider?)ServiceLocator.ServiceProvider.GetService(typeof(IFormatProvider));
19 _formatProvider1 = service;
20 }
21
22 return service;
23 }
24
25 set
26 {
27 throw new NotSupportedException();
28 }
29 }
30}
31
32internal class ServiceLocator : IServiceProvider
33{
34 private static readonly ServiceLocator _instance = new();
35 private readonly Dictionary<Type, object> _services = new();
36
37 public static IServiceProvider ServiceProvider => _instance;
38
39 object? IServiceProvider.GetService(Type serviceType)
40 {
41 this._services.TryGetValue(serviceType, out var value);
42
43 return value;
44 }
45
46 public static void AddService<T>(T service) where T : class
47 => _instance._services[typeof(T)] = service;
48}
Overriding several fields or properties from the same aspect
To override one or more fields or properties from a single aspect, implement the BuildAspect method and call the builder.Advice.Override method.
Alternatively, call the builder.Advice.OverrideAccessors method, which accepts one or two accessor templates: one template method for the getter and/or one for the setter.
Using a property template
The first argument of Override is the IFieldOrProperty that you want to override. This field or property must be in the type targeted by the current aspect instance.
The second argument of Override is the name of the template property. This property must exist in the aspect class and, additionally:
- The template property must be annotated with the
[Template]attribute. - The template property must be of type
dynamic(dynamically-typed template), or a type compatible with the type of the overridden property (strongly-typed template). - The template property can have a setter, a getter, or both. If one accessor isn't specified in the template, the corresponding accessor in the target code won't be overridden.
Example: Registry-backed class
The following aspect overrides properties so that they are written to and read from the Windows registry.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Microsoft.Win32;
4using System;
5using System.Linq;
6
7namespace Doc.RegistryStorage;
8
9internal class RegistryStorageAttribute : TypeAspect
10{
11 public string Key { get; }
12
13 public RegistryStorageAttribute( string key )
14 {
15 this.Key = "HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\" + key;
16 }
17
18 public override void BuildAspect( IAspectBuilder<INamedType> builder )
19 {
20 foreach ( var property in builder.Target.FieldsAndProperties.Where( p => p is
21 {
22 IsImplicitlyDeclared: false, IsAutoPropertyOrField: true
23 } ) )
24 {
25 builder.With( property ).Override( nameof(this.OverrideProperty) );
26 }
27 }
28
29 [Template]
30 private dynamic? OverrideProperty
31 {
32 get
33 {
34 var value = Registry.GetValue( this.Key, meta.Target.FieldOrProperty.Name, null );
35
36 if ( value != null )
37 {
38 return Convert.ChangeType( value, meta.Target.FieldOrProperty.Type.ToType() );
39 }
40 else
41 {
42 return default;
43 }
44 }
45
46 set
47 {
48 var stringValue = Convert.ToString( value );
49 Registry.SetValue( this.Key, meta.Target.FieldOrProperty.Name, stringValue );
50 meta.Proceed();
51 }
52 }
53}
1namespace Doc.RegistryStorage;
2
3[RegistryStorage( "Animals" )]
4internal class Animals
5{
6 public int Turtles { get; set; }
7
8 public int Cats { get; set; }
9
10 public int All => this.Turtles + this.Cats;
11}
1using System;
2using Microsoft.Win32;
3
4namespace Doc.RegistryStorage;
5
6[RegistryStorage("Animals")]
7internal class Animals
8{
9 private int _turtles;
10
11 public int Turtles
12 {
13 get
14 {
15 var value = Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Turtles", null);
16 if (value != null)
17 {
18 return (int)Convert.ChangeType(value, typeof(int));
19 }
20 else
21 {
22 return default;
23 }
24 }
25
26 set
27 {
28 var stringValue = Convert.ToString(value);
29 Registry.SetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Turtles", stringValue);
30 _turtles = value;
31 }
32 }
33
34 private int _cats;
35
36 public int Cats
37 {
38 get
39 {
40 var value = Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Cats", null);
41 if (value != null)
42 {
43 return (int)Convert.ChangeType(value, typeof(int));
44 }
45 else
46 {
47 return default;
48 }
49 }
50
51 set
52 {
53 var stringValue = Convert.ToString(value);
54 Registry.SetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Cats", stringValue);
55 _cats = value;
56 }
57 }
58
59 public int All => this.Turtles + this.Cats;
60}
Example: String normalization
This example illustrates a strongly-typed property template with a single accessor that uses the meta.Target.FieldOrProperty.Value expression to access the underlying field or property.
The following aspect can be applied to fields or properties of type string. It overrides the setter to trim and convert to lowercase the assigned value.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4namespace Doc.Normalize;
5
6internal class NormalizeAttribute : FieldOrPropertyAspect
7{
8 public override void BuildAspect( IAspectBuilder<IFieldOrProperty> builder )
9 {
10 builder.Override( nameof(this.OverrideProperty) );
11 }
12
13 [Template]
14 private string? OverrideProperty
15 {
16 set => meta.Target.FieldOrProperty.Value = value?.Trim().ToLowerInvariant();
17 }
18}
1namespace Doc.Normalize;
2
3internal class Foo
4{
5 [Normalize]
6 public string? Property { get; set; }
7}
1namespace Doc.Normalize;
2
3internal class Foo
4{
5 private string? _property;
6
7 [Normalize]
8 public string? Property
9 {
10 get
11 {
12 return _property;
13 }
14
15 set
16 {
17 _property = value?.Trim().ToLowerInvariant();
18 }
19 }
20}
Using an accessor template
The Override method has these limitations compared to OverrideAccessors:
- You cannot choose a template for each accessor separately.
- You cannot have generic templates.
To overcome these limitations, use the OverrideAccessors method and provide one or two method templates: a getter template and/or a setter template.
The templates must fulfill the following conditions:
- Both templates must be annotated with the
[Template]attribute. - The getter template must be of signature
T Getter(), whereTis eitherdynamicor a type compatible with the target field or property. - The setter template must be of signature
void Setter(T value), where the parameter namevalueis mandatory.
Example: Logging property setters
This example demonstrates OverrideAccessors with only a setter template. The aspect iterates over properties and overrides those with setters, skipping read-only properties like FullName.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4
5namespace Doc.LogSetters;
6
7internal class LogSettersAttribute : TypeAspect
8{
9 public override void BuildAspect( IAspectBuilder<INamedType> builder )
10 {
11 foreach ( var property in builder.Target.Properties )
12 {
13 // Only override properties that have a setter.
14 if ( property.Writeability != Writeability.None )
15 {
16 builder.With( property ).OverrideAccessors(
17 null,
18 nameof(this.SetterTemplate) );
19 }
20 }
21 }
22
23 [Template]
24 private void SetterTemplate( dynamic? value )
25 {
26 Console.WriteLine(
27 $"Setting {meta.Target.FieldOrProperty.Name} = {value}" );
28
29 meta.Proceed();
30 }
31}
32
1namespace Doc.LogSetters;
2
3[LogSetters]
4internal partial class Person
5{
6 public string? Name { get; set; }
7
8 public int Age { get; set; }
9
10 // This property has no setter, so the aspect skips it.
11 public string FullName => $"{Name} ({Age})";
12}
13
1using System;
2
3namespace Doc.LogSetters;
4
5[LogSetters]
6internal partial class Person
7{
8 private string? _name;
9
10 public string? Name
11 {
12 get
13 {
14 return _name;
15 }
16
17 set
18 {
19 Console.WriteLine($"Setting Name = {value}");
20 _name = value;
21 }
22 }
23
24 private int _age;
25
26 public int Age
27 {
28 get
29 {
30 return _age;
31 }
32
33 set
34 {
35 Console.WriteLine($"Setting Age = {value}");
36 _age = value;
37 }
38 }
39
40 // This property has no setter, so the aspect skips it.
41 public string FullName => $"{Name} ({Age})";
42}
43