Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Transforming Code / Overriding Fields or Properties

Overriding Fields or Properties

In Overriding Methods, you have learned how to wrap an existing method with additional, automatically-generated model. You can do the same with fields and properties thanks to the OverrideFieldOrPropertyAspect abstract class.

The simple way: deriving the OverrideFieldOrPropertyAspect abstract class

  1. Add Metalama to your project as described in Installing Metalama: Quick Start.

  2. Create a new class derived from the OverrideFieldOrPropertyAspect abstract class. This class will be a custom attribute, so it is a good idea to name it with the Attribute suffix.

  3. Implement the OverrideProperty property in plain C#. The accessors of this property will serve as templates defining the way the aspect overrides the accessors of the hand-written field or property.

    • To insert code or expressions that depend on the target accessors of the aspect (such as the field or property name or type), use the meta API.
    • Where the original implementation must be invoked, call the meta.Proceed method.
  4. The aspect is a custom attribute. To transform a field or property using the aspect, just add the aspect custom attribute to the field or property.

Warning

When you apply an aspect to a field, Metalama will automatically transform the field into a property. If the field is used by reference using ref, out and in keywords, it will result in a compile-time error. (TODO #28909)

Example: An empty OverrideFieldOrPropertyAspect aspect

The next example shows an empty implementation of OverrideFieldOrPropertyAspect applied to a property and to a field.

using Metalama.Framework.Aspects;

namespace Doc
{
    public class EmptyOverrideFieldOrPropertyAttribute : OverrideFieldOrPropertyAspect
    {
        public override dynamic? OverrideProperty
        {
            get => meta.Proceed();
            set => meta.Proceed();
        }
    }
}
namespace Doc
{
    internal class EmptyOverrideFieldOrPropertyExample
    {
        [EmptyOverrideFieldOrProperty]
        public int Field;

        [EmptyOverrideFieldOrProperty]
        public string? Property { get; set; }
    }
}
namespace Doc
{
    internal class EmptyOverrideFieldOrPropertyExample
    {


        private int _field;

        [EmptyOverrideFieldOrProperty]
        public int Field
        {
            get
            {
                return this._field;
            }

            set
            {
                this._field = value;
            }
        }

        private string? _property;

        [EmptyOverrideFieldOrProperty]
        public string? Property
        {
            get
            {
                return this._property;
            }

            set
            {
                this._property = value;
            }
        }
    }
}

Getting or setting the underlying property

If you have only worked with methods so far, you may be already used to use the meta.Proceed() method in your template. This method also works in a property template: when called from the getter, it returns the field or property value; when called from the setter, it sets the field or property to the value of the value parameter.

If you need to get the property value from the setter, or if you need to set the property value to something else than the value parameter, you can do it by getting or setting the meta.Target.FieldOrProperty.Value property.

TODO: example

Accessing the metadata of the overridden field or property

The metadata of the field or property being overridden are available from the template accessors on the meta.Target.FieldOrProperty property . This property gives you all information about the name, type, parameters and custom attributes of the field or property. For instance, the member name is available on meta.Target.FieldOrProperty.Name and its type on meta.Target.FieldOrProperty.Type.

The value of the field or property is available on meta.Target.FieldOrProperty.Value. Your aspect can read and write this property, as long as the field or the property is writable. To determine if the field is readonly or if the property has a set accessor, you can use Writeability.

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 make a call to a global service locator. The type of the service is determined from the type of the field or property, using meta.Target.FieldOrProperty.Type. The dependency is not stored, so the service locator must be called every time the property is evaluated.

using Metalama.Framework.Aspects;
using System;

namespace Doc.GlobalImport
{
    internal class ImportAttribute : OverrideFieldOrPropertyAspect
    {
        public override dynamic? OverrideProperty
        {
            get => ServiceLocator.ServiceProvider.GetService( meta.Target.FieldOrProperty.Type.ToType() );

            set => throw new NotSupportedException( $"{meta.Target.FieldOrProperty.Name} should not be set from source code." );
        }
    }
}
using System;
using System.Collections.Generic;

namespace Doc.GlobalImport
{
    internal class Foo
    {
        [Import]
        private IFormatProvider? FormatProvider { get; }
    }

    internal class ServiceLocator : IServiceProvider
    {
        private static readonly ServiceLocator _instance = new();
        private readonly Dictionary<Type, object> _services = new();

        public static IServiceProvider ServiceProvider => _instance;

        object? IServiceProvider.GetService( Type serviceType )
        {
            this._services.TryGetValue( serviceType, out var value );

            return value;
        }

        public static void AddService<T>( T service ) where T : class => _instance._services[typeof(T)] = service;
    }
}
using System;
using System.Collections.Generic;

namespace Doc.GlobalImport
{
    internal class Foo
    {
        [Import]
        private IFormatProvider? FormatProvider
        {
            get
            {
                return (IFormatProvider?)ServiceLocator.ServiceProvider.GetService(typeof(IFormatProvider));
            }

            set
            {
                throw new NotSupportedException($"FormatProvider should not be set from source code.");
            }
        }
    }

    internal class ServiceLocator : IServiceProvider
    {
        private static readonly ServiceLocator _instance = new();
        private readonly Dictionary<Type, object> _services = new();

        public static IServiceProvider ServiceProvider => _instance;

        object? IServiceProvider.GetService(Type serviceType)
        {
            this._services.TryGetValue(serviceType, out var value);

            return value;
        }

        public static void AddService<T>(T service) where T : class => _instance._services[typeof(T)] = service;
    }
}

Example: Resolving dependencies on the fly and storing the result

This example builds over the previous one, but the dependency is stored in the field or property after it has been retrieved from the service provider for the first time.

using Metalama.Framework.Aspects;
using System;

namespace Doc.GlobalImportWithSetter
{
    internal class ImportAttribute : OverrideFieldOrPropertyAspect
    {
        public override dynamic? OverrideProperty
        {
            get
            {
                // Gets the current value of the field or property.
                var service = meta.Proceed();

                if ( service == null )
                {
                    // Call the service provider.
                    service =
                        meta.Cast(
                            meta.Target.FieldOrProperty.Type,
                            ServiceLocator.ServiceProvider.GetService( meta.Target.Property.Type.ToType() ) );

                    // Set the field or property to the new value.
                    meta.Target.FieldOrProperty.Value = service;
                }

                return service;
            }

            set => throw new NotSupportedException();
        }
    }
}
using System;
using System.Collections.Generic;

namespace Doc.GlobalImportWithSetter
{
    internal class Foo
    {
        [Import]
        private IFormatProvider? _formatProvider;
    }

    internal class ServiceLocator : IServiceProvider
    {
        private static readonly ServiceLocator _instance = new();
        private readonly Dictionary<Type, object> _services = new();

        public static IServiceProvider ServiceProvider => _instance;

        object? IServiceProvider.GetService( Type serviceType )
        {
            this._services.TryGetValue( serviceType, out var value );

            return value;
        }

        public static void AddService<T>( T service ) where T : class => _instance._services[typeof(T)] = service;
    }
}
using System;
using System.Collections.Generic;

namespace Doc.GlobalImportWithSetter
{
    internal class Foo
    {


        private IFormatProvider? _formatProvider1;

        [Import]
        private IFormatProvider? _formatProvider
        {
            get
            {
                IFormatProvider? service;
                service = this._formatProvider1;
                goto __aspect_return_1;
            __aspect_return_1:
                if (service == null)
                {
                    service = (IFormatProvider?)ServiceLocator.ServiceProvider.GetService(typeof(IFormatProvider));
                    this._formatProvider = service;
                }

                return service;
            }

            set
            {
                throw new NotSupportedException();
            }
        }
    }

    internal class ServiceLocator : IServiceProvider
    {
        private static readonly ServiceLocator _instance = new();
        private readonly Dictionary<Type, object> _services = new();

        public static IServiceProvider ServiceProvider => _instance;

        object? IServiceProvider.GetService(Type serviceType)
        {
            this._services.TryGetValue(serviceType, out var value);

            return value;
        }

        public static void AddService<T>(T service) where T : class => _instance._services[typeof(T)] = service;
    }
}

Overriding several fields or properties from the same aspect

Just like for methods, to override one or more fields or properties from a single aspect, your aspect needs to implement the BuildAspect method exposed on builder.Advice. Your implementation must then call the <xref: Metalama.Framework.Advising.IAdviceFactory.OverrideFieldOrProperty*> method.

There are two overloads of this method:

  • One overload accepts a property template.
  • The second overload accepts one or two accessor templates, i.e. one template method for the getter and/or one other method for the setter.

Using a property template

The first argument of OverrideFieldOrProperty is the IFieldOrProperty that you want to override. This field or property must be in the type being targeted by the current aspect instance.

The second argument of OverrideFieldOrProperty 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 that is 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 is not specified in the template, the corresponding accessor in the target code will not be overridden.

Example: registry-backed class

The following aspect overrides properties so that they are written to and read from the Windows registry.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Microsoft.Win32;
using System;
using System.Linq;

namespace Doc.RegistryStorage
{
    internal class RegistryStorageAttribute : TypeAspect
    {
        public string Key { get; }

        public RegistryStorageAttribute( string key )
        {
            this.Key = "HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\" + key;
        }

        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            foreach ( var property in builder.Target.FieldsAndProperties.Where( p => p.IsAutoPropertyOrField ) )
            {
                builder.Advice.Override( property, nameof(this.OverrideProperty) );
            }
        }

        [Template]
        private dynamic? OverrideProperty
        {
            get
            {
                var type = meta.Target.FieldOrProperty.Type.ToType();
                var value = Registry.GetValue( this.Key, meta.Target.FieldOrProperty.Name, null );

                if ( value != null )
                {
                    return Convert.ChangeType( value, type );
                }
                else
                {
                    return meta.Target.FieldOrProperty.Type.DefaultValue();
                }
            }

            set
            {
                var stringValue = Convert.ToString( value );
                Registry.SetValue( this.Key, meta.Target.FieldOrProperty.Name, stringValue );
                meta.Proceed();
            }
        }
    }
}
namespace Doc.RegistryStorage
{
    [RegistryStorage( "Animals" )]
    internal class Animals
    {
        public int Turtles { get; set; }

        public int Cats { get; set; }

        public int All => this.Turtles + this.Cats;
    }
}
using System;
using Microsoft.Win32;

namespace Doc.RegistryStorage
{
    [RegistryStorage("Animals")]
    internal class Animals
    {


        private int _turtles;
        public int Turtles
        {
            get
            {
                var value = Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Turtles", null);
                if (value != null)
                {
                    return (int)Convert.ChangeType(value, typeof(int));
                }
                else
                {
                    return default;
                }
            }

            set
            {
                var stringValue = Convert.ToString(value);
                Registry.SetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Turtles", stringValue);
                this._turtles = value;
            }
        }


        private int _cats;

        public int Cats
        {
            get
            {
                var value = Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Cats", null);
                if (value != null)
                {
                    return (int)Convert.ChangeType(value, typeof(int));
                }
                else
                {
                    return default;
                }
            }

            set
            {
                var stringValue = Convert.ToString(value);
                Registry.SetValue("HKEY_CURRENT_USER\\SOFTWARE\\Company\\Product\\Animals", "Cats", stringValue);
                this._cats = value;
            }
        }

        public int All => this.Turtles + this.Cats;
    }
}

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 of properties of type string. It overrides the setter to trim and lower case the assigned value.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;

namespace Doc.Normalize
{
    internal class NormalizeAttribute : FieldOrPropertyAspect
    {
        public override void BuildAspect( IAspectBuilder<IFieldOrProperty> builder )
        {
            builder.Advice.Override( builder.Target, nameof(this.OverrideProperty) );
        }

        [Template]
        private string OverrideProperty
        {
            set => meta.Target.FieldOrProperty.Value = value?.Trim().ToLowerInvariant();
        }
    }
}
namespace Doc.Normalize
{
    internal class Foo
    {
        [Normalize]
        public string? Property { get; set; }
    }
}
namespace Doc.Normalize
{
    internal class Foo
    {
        [Normalize]
        public string? Property
        {
            get
            {
                return this.Property_Source;
            }

            set
            {
                this.Property_Source = value?.Trim().ToLowerInvariant();
            }
        }

        private string? Property_Source { get; set; }
    }
}

Using an accessor template

Advising fields or properties with the OverrideFieldOrProperty has the following limitations over the use of OverrideAccessors:

  • You cannot choose a template for each accessor separately.
  • You cannot have generic templates. (Not yet implemented in Overrideccessors anyway.)

To alleviate these limitations, you can use the method Override 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(), where T is either dynamic or a type compatible with the target field or property.
  • The setter template must be of signature void Setter(T value), where the name value of the first parameter is mandatory.

TODO: example