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}
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
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>
orIAspectBuilder<ICompilation>
and want to add a type toMy.Namespace
, call theWithNamespace("My.Namespace")
extension method. - If you don't have an
IAdviser<ICompilation>
, callaspectBuilder.With(aspectBuilder.Target.Compilation)
, then callWithNamespace
. - 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}
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
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}
1namespace Doc.IntroduceNestedClass_Members;
2
3[Builder]
4internal class Material
5{
6 public string Name { get; }
7
8 public double Density { get; }
9}
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}
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}
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.