Project "Caravela" 0.3 / / Caravela Documentation / Conceptual Documentation / Creating Aspects / Advising Code / Introducing Members

Introducing Members

In Overriding Members (Reloaded), you have learned how to override the implementation of existing type members. In this article, you will learn how to add new members to an existing type.

You can currently add the following kinds of members:

  • methods,
  • fields,
  • properties,
  • events.

However, the following kind of members are not yet supported:

  • operators,
  • conversions,
  • constructors.

Introducing members declaratively

The easiest way to introduce a member from an aspect is to implement this member in the aspect and annotate it with the [Introduce] custom attribute. This custom attribute has the following interesting properties:

Property Description
Name Sets the name of the introduced member. If not specified, the name of the introduced member is the name of the template itself.
Scope Decides whether the introduced member will be static or not. See IntroductionScope for possible strategies. By default, it is copied from the template, except when the aspect is applied to a static member, in which case the introduced member is always static.
Accessibility Determines if the member will be private, protected, public, etc. By default, the accessibility of the template is copied.
IsVirtual Determines if the member will be virtual. By default, the characteristic of the template is copied.
IsSealed Determines if the member will be sealed. By default, the characteristic of the template is copied.

Example: ToString

The following example shows an aspect that implements the ToString method. It will return a string including the value of all fields.

Note that this aspect will replace any hand-written implementation of ToString, which is not desirable. It can can only be avoided by introducing the method programmatically and conditionally (TODO 28807).

                using System;
using System.Linq;
using System.Text;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;
using Caravela.Framework.Code.SyntaxBuilders;

namespace Caravela.Documentation.SampleCode.AspectFramework.ToString
{
    class ToStringAttribute : TypeAspect
    {
        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
        public string IntroducedToString()
        {
            var stringBuilder = new InterpolatedStringBuilder();
            stringBuilder.AddText("{ ");
            stringBuilder.AddText(meta.Target.Type.Name );
            stringBuilder.AddText(" ");

            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsStatic ).ToList();

            var i = meta.CompileTime( 0 );

            foreach ( var field in fields)
            {
                if ( i > 0 )
                {
                    stringBuilder.AddText(", ");
                }

                stringBuilder.AddText(field.Name);
                stringBuilder.AddText("=");
                stringBuilder.AddExpression(field.Invokers.Final.GetValue(meta.This) );

                i++;
            }

            stringBuilder.AddText(" }");


            return stringBuilder.ToValue();

        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.ToString
{
    [ToString]
    class TargetCode
    {
        int _x;

        public string? Y { get; set; }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.ToString
{
    [ToString]
    class TargetCode
    {
        int _x;

        public string? Y { get; set; }


        public override string ToString()
        {
            return $"{{ TargetCode _x={_x}, Y={Y} }";
        }
    }
}
              

Introducing members programmatically

The principal limitation of declarative introductions is that the name, type and signature of the introduced member must be known upfront. They cannot depend on the target aspect. The programmatic approach allows your aspect to completely customize the declaration based on the target code.

There are three steps to introduce a member programmatically:

Step 1. Implement the template

Implement the template in your aspect class and annotate it with the [Template] custom attribute. The template does not need to have the final signature.

Step 2. Invoke IAdviceFactory.Introduce*

In your implementation of the BuildAspect method, call one of the following methods and store the return value in a variable.

A call to these method creates a member that has the same characteristics as the template (name, signature, ...), taking into account the properties of the [Template] custom attribute. However, they return a builder object that allows you to modify these characteristics.

Step 3. Adjust the name and signature using the builder

Modify the name and signature of the introduced declaration as needed using the builder object returned by the advice factory method.

Example: Update method

The following aspect introduces an Update method that assigns all writable field in the target type. The method signature is dynamic: there is one parameter per writable field or property.

                using System;
using System.Linq;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;

namespace Caravela.Documentation.SampleCode.AspectFramework.UpdateMethod
{
    class UpdateMethodAttribute : TypeAspect
    {
        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            var updateMethodBuilder = builder.Advices.IntroduceMethod(builder.Target, nameof(Update));

            var fieldsAndProperties =
                builder.Target.FieldsAndProperties
                .Where(f => f.Writeability == Writeability.All);

            foreach ( var field in fieldsAndProperties)
            {
                updateMethodBuilder.AddParameter(field.Name, field.Type);
            }
        }

        [Template]
        public void Update()
        {
            var index = meta.CompileTime(0);

            foreach ( var parameter in meta.Target.Parameters )
            {
                var field = meta.Target.Type.FieldsAndProperties.OfName(parameter.Name).Single();

                field.Invokers.Final.SetValue(meta.This, meta.Target.Parameters[index].Value);
                index++;
            }
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.UpdateMethod
{
    [UpdateMethod]
    class CityHunter
    {
        int _x;

        public string? Y { get; private set; }

        public DateTime Z { get; }
    }

    class Program
    {
        static void Main()
        {
            CityHunter ch = new();
            ch.Update(0, "1");
        }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.UpdateMethod
{
    [UpdateMethod]
    class CityHunter
    {
        int _x;

        public string? Y { get; private set; }

        public DateTime Z { get; }


        public void Update(int _x, string? Y)
        {
            this._x = _x;
            this.Y = Y;
        }
    }

    class Program
    {
        static void Main()
        {
            CityHunter ch = new();
            ch.Update(0, "1");
        }
    }
}
              

Overriding existing implementations

Specifying the override strategy

When you want to introduce a member to a type, it may happen that the same member is already defined in this type or in a parent type. The default strategy of the aspect framework in this case it simply to report an error and fail the build. You can change this behavior by setting the OverrideStrategy for this advice:

  • For declarative advices, set the WhenExists property of the custom attribute,
  • For programmatic advices, set the whenExists optional parameter of the advice factory method.

Accessing the overridden declaration

Most of the times, when you override a method, you will want to invoke the aspect to invoke the base implementation. The same applies to properties and events. In plain C#, when you override a base-class member in a derived class, you call the member with the base prefix. A similar approach exists in Caravela.

Referencing introduced members in template

When you introduce a member to a type, your will often want to access it from templates. There are three ways to do it:

Option 1. Access the aspect template member

                using System;
using System.ComponentModel;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;

namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged1
{
    class IntroducePropertyChangedAspect : TypeAspect
    {
        [Introduce]
        public event PropertyChangedEventHandler? PropertyChanged;

        [Introduce]
        protected virtual void OnPropertyChanged( string propertyName )
        {
            this.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(propertyName));
        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged1
{
    [IntroducePropertyChangedAspect]
    class TargetCode
    {
    }
}

              
                using System.ComponentModel;

namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged1
{
    [IntroducePropertyChangedAspect]
    class TargetCode
    {


        protected void OnPropertyChanged(string propertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler? PropertyChanged;
    }
}
              

Option 2. Use meta.This and write dynamic code

                using System;
using System.ComponentModel;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;

namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged3
{
    class IntroducePropertyChangedAspect : TypeAspect
    {
        [Introduce]
        public event PropertyChangedEventHandler? PropertyChanged;

        [Introduce]
        protected virtual void OnPropertyChanged( string propertyName )
        {
            meta.This.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(propertyName));
        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged3
{
    [IntroducePropertyChangedAspect]
    class TargetCode
    {
    }
}

              
                using System.ComponentModel;

namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged3
{
    [IntroducePropertyChangedAspect]
    class TargetCode
    {


        protected void OnPropertyChanged(string propertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler? PropertyChanged;
    }
}
              

Option 3. Use the invoker of the builder object

If none of the approaches above offer you the required flexibility (typically because the name of the introduced member is dynamic), use the invokers exposed on the builder object returned from the advice factory method.

Note

Declarations introduced by an aspect or aspect layer are not visible in the meta code model exposed to in the same aspect or aspect layer. To reference builders, you have to reference them differently. For details, see Sharing State with Advices.

For details, see Caravela.Framework.Code.Invokers

                using System;
using System.ComponentModel;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;

namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged2
{
    class IntroducePropertyChangedAspect : TypeAspect
    {
        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            var eventBuilder = builder.Advices.IntroduceEvent(
                builder.Target,
                nameof(PropertyChanged));

            builder.Advices.IntroduceMethod(
                builder.Target,
                nameof(OnPropertyChanged),
                tags: new () {  ["event"] = eventBuilder });
        }


        [Template]
        public event PropertyChangedEventHandler? PropertyChanged;

        [Template]
        protected virtual void OnPropertyChanged( string propertyName )
        {
            ((IEvent) meta.Tags["event"]!).Invokers.Final.Raise(
                meta.This,
                meta.This, new PropertyChangedEventArgs(propertyName));
        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged2
{
    [IntroducePropertyChangedAspect]
    class TargetCode
    {
    }
}

              
                using System.ComponentModel;

namespace Caravela.Documentation.SampleCode.AspectFramework.IntroducePropertyChanged2
{
    [IntroducePropertyChangedAspect]
    class TargetCode
    {


        protected void OnPropertyChanged(string propertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler? PropertyChanged;
    }
}
              

Referencing introduced members from source code

If you want the source code (not your aspect code) to reference declarations introduced by your aspect, the user of your aspect needs to make the target types partial. Without this keyword, the introduced declarations will not be visible at design time in syntax completion, and the IDE will report errors. Note that the compiler will not complain because Caravela replaces the compiler, but the IDE will because it does not know about Caravela, and here Caravela, and therefore your aspect, has to follow the rules of the C# compiler. How inconvenient it may be, there is nothing you as an aspect author, or us as the authors of Caravela, can do.

If the user does not add the partial keyword, Caravela will report a warning and offer a code fix.

Note

In test projects built using Caravela.TestFramework, the Caravela compiler is not activated. Therefore, the source code of test projects cannot reference introduced declarations. Since the present documentation relies on Caravela.TestFramework for all examples, we cannot include an example here.