Open sandboxFocusImprove this doc

ToString example, step 1: Getting started

This example gives the simplest possible implementation of the [ToString] aspect, which generates the ToString method based on public fields and properties of the current type.

Our objective is to be able to generate code like this:

Source Code


1[ToString]
2internal class MovingVertex
3{
4    public double X;
5
6    public double Y;
7
8    public double DX;
9
10    public double DY { get; set; }
11
12    public double Velocity
13        => Math.Sqrt( (this.DX * this.DX) + (this.DY * this.DY) );




14}
Transformed Code
1using System;
2
3[ToString]
4internal class MovingVertex
5{
6    public double X;
7
8    public double Y;
9
10    public double DX;
11
12    public double DY { get; set; }
13
14    public double Velocity
15        => Math.Sqrt( (this.DX * this.DX) + (this.DY * this.DY) );
16public override string ToString()
17    {
18        return $"{{ MovingVertex DX={DX}, DY={DY}, Velocity={Velocity}, X={X}, Y={Y} }}";
19    }
20}

Aspect implementation

The aspect is quite straightforward. Here its complete source code:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4
5internal class ToStringAttribute : TypeAspect
6{
7    [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
8    public string IntroducedToString()
9    {
10        var stringBuilder = new InterpolatedStringBuilder();
11        stringBuilder.AddText( "{ " );
12        stringBuilder.AddText( meta.Target.Type.Name );
13        stringBuilder.AddText( " " );
14
15        var properties = meta.Target.Type.AllFieldsAndProperties
16            .Where( f => f is
17            {
18                IsStatic: false, IsImplicitlyDeclared: false, Accessibility: Accessibility.Public
19            } )
20            .OrderBy( f => f.Name );
21
22        
23        var i = meta.CompileTime( 0 ); 
24
25        foreach ( var property in properties )
26        {
27            if ( i > 0 )
28            {
29                stringBuilder.AddText( ", " );
30            }
31
32            stringBuilder.AddText( property.Name );
33            stringBuilder.AddText( "=" );
34            stringBuilder.AddExpression( property );
35
36            i++;
37        }
38
39        stringBuilder.AddText( " }" );
40
41        return stringBuilder.ToValue();
42    }
43}

Because we want to apply this aspect to types, we derive the ToStringAttribute class from TypeAspect.

The only member of this type is the IntroducedToString method: the template for the new ToString method.

We use [Introduce] on the top of this method to mean that the method must be introduced to the target class.

Note that we cannot name this template method ToString because the ToString method already exists in the object class and is obviously not a template. Therefore, we must set the Name property of [Introduce] to say that the name of the introduced method differs from the name of the template method.

We use WhenExists = OverrideStrategy.Override to ask Metalama to override the method if it already exists in the base type, which is obviously always the case.

The template implementation relies on InterpolatedStringBuilder to generate an interpolated string. Note that InterpolatedStringBuilder is a compile-time class.

The following line may look weird at first sight:

22        
23        var i = meta.CompileTime( 0 ); 

It defines a compile-time variable. Without the call to meta.CompileTime, a run-time local variable would be defined because, when an expression can be both run-time or compile-time (as can be 0), it is considered run-time by default.

We enumerate the AllFieldsAndProperties collection, which contains all fields and properties of the current type, including all those inherited from the base type (the FieldsAndProperties) only contains members of the current type, ignoring those of the base type).

For each public field or property, we call AddText to add the member name, then AddExpression to add the member value.

At the end of the method, we call the ToValue method to build a run-time interpolated string from the compile-time InterpolatedStringBuilder, which is the return value of our ToString method.