Open sandboxFocusImprove this doc

Introducing types

Many patterns require you to create new types. This is the case, for instance, with the Memento, Enum View-Model, or Builder patterns. You can do this by calling the IntroduceClass or IntroduceInterface advice method from your BuildAspect implementation.

Note

The current version of Metalama allows you to introduce classes and interfaces. Support for structs, delegates, and enums will be added in a future release.

Introducing a nested class

To introduce a nested class, call the IntroduceClass or IntroduceInterface method from an IAdviser<INamedType>. For instance, if you have a TypeAspect, just call aspectBuilder.IntroduceClass( "Foo" ).

Example: nested class

In the following example, the aspect introduces a nested class named Factory.

1#pragma warning disable CA1725
2
3using Metalama.Framework.Advising;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using System.Linq;
7
8namespace Doc.IntroduceNestedClass;
9
10[Inheritable]
11public class BuilderAttribute : TypeAspect
12{
13    public override void BuildAspect( IAspectBuilder<INamedType> builder )
14    {
15        base.BuildAspect( builder );
16        
17        // Find the Builder class of the base class, if any.
18        var baseBuilderClass =
19            builder.Target.BaseType?.Types.OfName( "Builder" ).SingleOrDefault();
20
21        // Introduce a public nested type.
22        builder.IntroduceClass(
23            "Builder",
24            OverrideStrategy.New,
25            buildType:
26            type =>
27            {
28                type.Accessibility = Accessibility.Public;
29                type.BaseType = baseBuilderClass;
30            } );
31    }
32}
Source Code
1namespace Doc.IntroduceNestedClass;
2
3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9}
10




11internal class Metal : Material
12{
13    public double MeltingPoint { get; }
14
15    public double ElectricalConductivity { get; }
16}
17
Transformed Code
1namespace Doc.IntroduceNestedClass;
2
3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9
10    public class Builder
11    {
12    }
13}
14
15internal class Metal : Material
16{
17    public double MeltingPoint { get; }
18
19    public double ElectricalConductivity { get; }
20
21    public new class Builder : Material.Builder
22    {
23    }
24}
25

Introducing a top-level class

To introduce a non-nested class, you must first get hold of an IAdviser<INamespace>. Here are a few strategies to get a namespace adviser from any IAdviser<T> or IAspectBuilder<TAspectTarget>:

  • If you have an IAdviser<ICompilation> or IAspectBuilder<ICompilation> and want to add a type to My.Namespace, call the WithNamespace("My.Namespace") extension method.
  • If you don't have an IAdviser<ICompilation>, call aspectBuilder.With(aspectBuilder.Target.Compilation), then call WithNamespace.
  • To get an adviser for the current namespace, call aspectBuilder.With(aspectBuilder.Target.GetNamespace()).
  • To get an adviser for a child of the current namespace, call aspectBuilder.With(aspectBuilder.Target.GetNamespace()).WithChildNamespace("ChildNs").

Once you have an IAdviser<INamespace>, call the IntroduceClass advice method.

Example: top-level class

In the following example, the aspect introduces a class in the Builders child namespace of the target class's namespace.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.IntroduceTopLevelClass;
6
7public class BuilderAttribute : TypeAspect
8{
9    public override void BuildAspect( IAspectBuilder<INamedType> builder )
10    {
11        base.BuildAspect( builder );
12
13        var builderType = builder
14            .With( builder.Target.GetNamespace()! )
15            .WithChildNamespace( "Builders" )
16            .IntroduceClass( builder.Target.Name + "Builder" );
17    }
18}
1namespace Doc.IntroduceTopLevelClass;
2
3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9}
namespace Doc.IntroduceTopLevelClass.Builders
{
  class MaterialBuilder
  {
  }
}

Adding class modifiers, attributes, base class, and type parameters

By default, the IntroduceClass method introduces a non-generic class with no modifiers or custom attributes, derived from object. To add modifiers, custom attributes, a base type, or type parameters, you must supply a delegate of type Action<INamedTypeBuilder> to the buildType parameter of the IntroduceClass method. This delegate receives an INamedTypeBuilder, which exposes the required APIs.

Example: setting up the type

In the following aspect, we continue the nested type example, make it public, and set its base type to the Builder nested type of the base class, if any.

1#pragma warning disable CA1725
2
3using Metalama.Framework.Advising;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using System.Linq;
7
8namespace Doc.IntroduceNestedClass;
9
10[Inheritable]
11public class BuilderAttribute : TypeAspect
12{
13    public override void BuildAspect( IAspectBuilder<INamedType> builder )
14    {
15        base.BuildAspect( builder );
16        
17        // Find the Builder class of the base class, if any.
18        var baseBuilderClass =
19            builder.Target.BaseType?.Types.OfName( "Builder" ).SingleOrDefault();
20
21        // Introduce a public nested type.
22        builder.IntroduceClass(
23            "Builder",
24            OverrideStrategy.New,
25            buildType:
26            type =>
27            {
28                type.Accessibility = Accessibility.Public;
29                type.BaseType = baseBuilderClass;
30            } );
31    }
32}
Source Code
1namespace Doc.IntroduceNestedClass;
2
3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9}
10




11internal class Metal : Material
12{
13    public double MeltingPoint { get; }
14
15    public double ElectricalConductivity { get; }
16}
17
Transformed Code
1namespace Doc.IntroduceNestedClass;
2
3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9
10    public class Builder
11    {
12    }
13}
14
15internal class Metal : Material
16{
17    public double MeltingPoint { get; }
18
19    public double ElectricalConductivity { get; }
20
21    public new class Builder : Material.Builder
22    {
23    }
24}
25

Adding class members

Once you introduce the type, the next step is to introduce members: constructors, methods, fields, properties, etc.

Introduced types work exactly like source-defined ones.

When you call IntroduceClass, it returns an IClassIntroductionAdviceResult. This interface derives from IAdviser<INamedType>, which has familiar extension methods like IntroduceMethod, IntroduceField, IntroduceProperty and so on.

Note

All programmatic techniques described in Introducing members also work with introduced types through the IAdviser<INamedType> interface.

Example: adding properties

The following aspect copies the properties of the source object into the introduced Builder type.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.Linq;
5
6namespace Doc.IntroduceNestedClass_Members;
7
8public class BuilderAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        base.BuildAspect( builder );
13
14        // Introduce a nested type.
15        var nestedType = builder.IntroduceClass( "Builder" );
16
17        // Introduce properties.
18        var properties =
19            builder.Target.Properties.Where(
20                p => p.Writeability != Writeability.None && !p.IsStatic );
21
22        foreach ( var property in properties )
23        {
24            nestedType.IntroduceAutomaticProperty(
25                property.Name,
26                property.Type );
27        }
28    }
29}
Source Code
1namespace Doc.IntroduceNestedClass_Members;
2

3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9}
Transformed Code
1namespace Doc.IntroduceNestedClass_Members;
2
3[Builder]
4internal class Material
5{
6    public string Name { get; }
7
8    public double Density { get; }
9
10    class Builder
11    {
12        private double _density;
13
14        private double Density
15        {
16            get
17            {
18                return _density;
19            }
20
21            set
22            {
23                _density = value;
24            }
25        }
26
27        private string _name = default!;
28
29        private string Name
30        {
31            get
32            {
33                return _name;
34            }
35
36            set
37            {
38                _name = value;
39            }
40        }
41    }
42}

Adding implemented interfaces

To add interface implementations to an introduced type, use the ImplementInterface method as mentioned in Implementing interfaces.

Final example: the Builder pattern

Let's finish this article with a complete implementation of the Builder pattern, a few fragments of which were illustrated above.

The input code for this pattern is an anemic class with get-only automatic properties.

The Builder aspect generates the following artifacts:

  • A Builder nested class with:
    • A public constructor accepting all required properties,
    • Writable properties corresponding to all automatic properties of the source class,
    • A Build method that instantiates the source type,
  • A private constructor in the source class, called by the Builder.Build method.

Ideally, the aspect should also test that the source type does not have another constructor or any settable property, but this is skipped in this example.

A key element of the design in the aspect is the PropertyMapping record, which maps a property of the source type to the corresponding property in the Builder type, the corresponding constructor parameter in the Builder type, and the corresponding parameter in the source type. We build this list in the BuildAspect method.

We use the aspectBuilder.Tags property to share this list with the template implementations, which can then read it from meta.Tags.Source.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.Collections.Generic;
5using System.ComponentModel.DataAnnotations;
6using System.Linq;
7
8namespace Doc.Builder_;
9
10public class BuilderAttribute : TypeAspect
11{
12    [CompileTime]
13    private class PropertyMapping
14    {
15        public PropertyMapping( IProperty sourceProperty, bool isRequired )
16        {
17            this.SourceProperty = sourceProperty;
18            this.IsRequired = isRequired;
19        }
20
21        public IProperty SourceProperty { get; }
22
23        public bool IsRequired { get; }
24
25        public IProperty? BuilderProperty { get; set; }
26
27        public int? SourceConstructorParameterIndex { get; set; }
28
29        public int? BuilderConstructorParameterIndex { get; set; }
30    }
31
32    [CompileTime]
33    private record Tags(
34        IReadOnlyList<PropertyMapping> Properties,
35        IConstructor SourceConstructor );
36
37    public override void BuildAspect( IAspectBuilder<INamedType> builder )
38    {
39        base.BuildAspect( builder );
40
41        // Create a list of PropertyMapping items for all properties that we want to build using the Builder.
42        var properties = builder.Target.Properties.Where(
43                p => p.Writeability != Writeability.None &&
44                     !p.IsStatic )
45            .Select(
46                p => new PropertyMapping(
47                    p,
48                    p.Attributes.OfAttributeType( typeof(RequiredAttribute) ).Any() ) )
49            .ToList();
50
51        // Introduce the Builder nested type.
52        var builderType = builder.IntroduceClass(
53            "Builder",
54            buildType: t => t.Accessibility = Accessibility.Public );
55
56        // Add builder properties and update the mapping.
57        foreach ( var property in properties )
58        {
59            property.BuilderProperty =
60                builderType.IntroduceAutomaticProperty(
61                        property.SourceProperty.Name,
62                        property.SourceProperty.Type,
63                        IntroductionScope.Instance )
64                    .Declaration;
65        }
66
67        // Add a builder constructor accepting the required properties and update the mapping.
68        if ( properties.Any( m => m.IsRequired ) )
69        {
70            builderType.IntroduceConstructor(
71                nameof(this.BuilderConstructorTemplate),
72                buildConstructor: c =>
73                {
74                    foreach ( var property in properties.Where( m => m.IsRequired ) )
75                    {
76                        property.BuilderConstructorParameterIndex = c.AddParameter(
77                                property.SourceProperty.Name,
78                                property.SourceProperty.Type )
79                            .Index;
80                    }
81                } );
82        }
83
84        // Add a Build method to the builder.
85        builderType.IntroduceMethod(
86            nameof(this.BuildMethodTemplate),
87            IntroductionScope.Instance,
88            buildMethod: m =>
89            {
90                m.Name = "Build";
91                m.Accessibility = Accessibility.Public;
92                m.ReturnType = builder.Target;
93
94                foreach ( var property in properties )
95                {
96                    property.BuilderConstructorParameterIndex =
97                        m.AddParameter( property.SourceProperty.Name, property.SourceProperty.Type )
98                            .Index;
99                }
100            } );
101
102        // Add a constructor to the source type with all properties.
103        var constructor = builder.IntroduceConstructor(
104                nameof(this.SourceConstructorTemplate),
105                buildConstructor: c =>
106                {
107                    c.Accessibility = Accessibility.Private;
108
109                    foreach ( var property in properties )
110                    {
111                        property.SourceConstructorParameterIndex = c.AddParameter(
112                                property.SourceProperty.Name,
113                                property.SourceProperty.Type )
114                            .Index;
115                    }
116                } )
117            .Declaration;
118
119        builder.Tags = new Tags( properties, constructor );
120    }
121
122    [Template]
123    private void BuilderConstructorTemplate()
124    {
125        var tags = (Tags) meta.Tags.Source!;
126
127        foreach ( var property in tags.Properties.Where( p => p.IsRequired ) )
128        {
129            property.BuilderProperty!.Value =
130                meta.Target.Parameters[property.BuilderConstructorParameterIndex!.Value].Value;
131        }
132    }
133
134    [Template]
135    private void SourceConstructorTemplate()
136    {
137        var tags = (Tags) meta.Tags.Source!;
138
139        foreach ( var property in tags.Properties )
140        {
141            property.BuilderProperty!.Value =
142                meta.Target.Parameters[property.SourceConstructorParameterIndex!.Value].Value;
143        }
144    }
145
146    [Template]
147    private dynamic BuildMethodTemplate()
148    {
149        var tags = (Tags) meta.Tags.Source!;
150
151        return tags.SourceConstructor.Invoke( tags.Properties.Select( x => x.BuilderProperty! ) )!;
152    }
153}
Source Code
1using System.ComponentModel.DataAnnotations;
2
3namespace Doc.Builder_;
4
5[Builder]
6internal class Material
7{
8    [Required]
9    public string Name { get; }
10
11    public double Density { get; }
12}
Transformed Code
1using System.ComponentModel.DataAnnotations;
2
3namespace Doc.Builder_;
4
5[Builder]
6internal class Material
7{
8    [Required]
9    public string Name { get; }
10
11    public double Density { get; }
12
13    private Material(string Name, double Density)
14    {
15        this.Name = Name;
16        this.Density = Density;
17    }
18
19    public class Builder
20    {
21        private Builder(string Name)
22        {
23            this.Name = Name;
24        }
25
26        private double _density;
27
28        private double Density
29        {
30            get
31            {
32                return _density;
33            }
34
35            set
36            {
37                _density = value;
38            }
39        }
40
41        private string _name = default!;
42
43        private string Name
44        {
45            get
46            {
47                return _name;
48            }
49
50            set
51            {
52                _name = value;
53            }
54        }
55
56        public Material Build(string Name, double Density)
57        {
58            return new Material(this.Name, this.Density);
59        }
60    }
61}
Note

For more about the Builder pattern, see Implementing the Builder pattern without boilerplate.