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, interfaces, and extension blocks. 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.Aspects;
4using Metalama.Framework.Code;
5using System.Linq;
6
7namespace Doc.IntroduceNestedClass;
8
9[Inheritable]
10public class BuilderAttribute : TypeAspect
11{
12 public override void BuildAspect( IAspectBuilder<INamedType> builder )
13 {
14 base.BuildAspect( builder );
15
16 // Find the Builder class of the base class, if any.
17 var baseBuilderClass =
18 builder.Target.BaseType?.Types.OfName( "Builder" ).SingleOrDefault();
19
20 // Introduce a public nested type.
21 builder.IntroduceClass(
22 "Builder",
23 OverrideStrategy.New,
24 buildType:
25 type =>
26 {
27 type.Accessibility = Accessibility.Public;
28 type.BaseType = baseBuilderClass;
29 } );
30 }
31}
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}
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}
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 do not 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.Aspects;
2using Metalama.Framework.Code;
3
4namespace Doc.IntroduceTopLevelClass;
5
6public class BuilderAttribute : TypeAspect
7{
8 public override void BuildAspect( IAspectBuilder<INamedType> builder )
9 {
10 base.BuildAspect( builder );
11
12 var builderType = builder
13 .With( builder.Target.GetNamespace()! )
14 .WithChildNamespace( "Builders" )
15 .IntroduceClass( builder.Target.Name + "Builder" );
16 }
17}
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.Aspects;
4using Metalama.Framework.Code;
5using System.Linq;
6
7namespace Doc.IntroduceNestedClass;
8
9[Inheritable]
10public class BuilderAttribute : TypeAspect
11{
12 public override void BuildAspect( IAspectBuilder<INamedType> builder )
13 {
14 base.BuildAspect( builder );
15
16 // Find the Builder class of the base class, if any.
17 var baseBuilderClass =
18 builder.Target.BaseType?.Types.OfName( "Builder" ).SingleOrDefault();
19
20 // Introduce a public nested type.
21 builder.IntroduceClass(
22 "Builder",
23 OverrideStrategy.New,
24 buildType:
25 type =>
26 {
27 type.Accessibility = Accessibility.Public;
28 type.BaseType = baseBuilderClass;
29 } );
30 }
31}
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}
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}
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 IIntroductionAdviceResult<T>. 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.Aspects;
2using Metalama.Framework.Code;
3using System.Linq;
4
5namespace Doc.IntroduceNestedClass_Members;
6
7public class BuilderAttribute : TypeAspect
8{
9 public override void BuildAspect( IAspectBuilder<INamedType> builder )
10 {
11 base.BuildAspect( builder );
12
13 // Introduce a nested type.
14 var nestedType = builder.IntroduceClass( "Builder" );
15
16 // Introduce properties.
17 var properties =
18 builder.Target.Properties.Where( p => p.Writeability != Writeability.None
19 && !p.IsStatic );
20
21 foreach ( var property in properties )
22 {
23 nestedType.IntroduceAutomaticProperty(
24 property.Name,
25 property.Type );
26 }
27 }
28}
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 { get; set; }
13
14 private string Name { get; set; }
15 }
16}
Adding implemented interfaces
To add interface implementations to an introduced type, use the ImplementInterface method as mentioned in Implementing interfaces.
Introducing extension blocks
Starting with Metalama 2026.1, you can introduce C# 14 extension blocks using the IntroduceExtensionBlock method. Extension blocks allow you to add extension members (methods, properties, operators) to any type.
To introduce an extension block:
- Ensure the extension block is introduced into a static class (extension blocks must be defined in static classes). The aspect itself can be applied to any supported target type and can introduce that static class if needed.
- Call IntroduceExtensionBlock on that static class, specifying the receiver type (the type being extended) and an optional receiver parameter name.
- Use the returned IIntroductionAdviceResult<T> to introduce members into the extension block, just as you would with an introduced class.
Setting the receiverParameterName to a non-empty string (e.g., "self") creates an instance extension block, where introduced members appear as instance members of the extended type. Setting it to null or an empty string creates a static extension block.
Example: Extension block
In the following example, the [GenerateToDisplayString] aspect is applied to an enum type. It introduces a new top-level static class FruitExtensions, adds an extension block for the Fruit enum, and introduces a ToDisplayString() extension method.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4namespace Doc.IntroduceExtensionBlock;
5
6public class GenerateToDisplayStringAttribute : TypeAspect
7{
8 public override void BuildAspect( IAspectBuilder<INamedType> builder )
9 {
10 // Introduce a top-level static class named <TargetType>Extensions.
11 var ns = builder.With( builder.Target.Compilation )
12 .WithNamespace( builder.Target.ContainingNamespace.FullName );
13
14 var extensionsClass = ns.IntroduceClass(
15 builder.Target.Name + "Extensions",
16 buildType: t => t.IsStatic = true );
17
18 // Introduce an instance extension block for the target enum type.
19 var extensionBlock =
20 extensionsClass.IntroduceExtensionBlock( builder.Target, "self" );
21
22 // Introduce the ToDisplayString method into the extension block.
23 extensionBlock.IntroduceMethod( nameof(ToDisplayString) );
24 }
25
26 [Template]
27 public string ToDisplayString()
28 {
29 // A complete implementation would use SwitchStatementBuilder to generate
30 // a switch expression mapping each member to a display string.
31 // See the "Generating switch statements" article for details.
32 return "unknown";
33 }
34}
35
1namespace Doc.IntroduceExtensionBlock;
2
3[GenerateToDisplayString]
4public enum Fruit
5{
6 Apple,
7 Banana,
8 Cherry
9}
10
namespace Doc.IntroduceExtensionBlock
{
static class FruitExtensions
{
extension(Fruit self)
{
public string ToDisplayString()
{
return "unknown";
}
}
}
}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
Buildernested class with:- A public constructor accepting all required properties.
- Writable properties corresponding to all automatic properties of the source class.
- A
Buildmethod that instantiates the source type.
- A private constructor in the source class that's called by the
Builder.Buildmethod.
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( p => p.Writeability != Writeability.None
43 &&
44 !p.IsStatic )
45 .Select( p => new PropertyMapping(
46 p,
47 p.Attributes.OfAttributeType( typeof(RequiredAttribute) ).Any() ) )
48 .ToList();
49
50 // Introduce the Builder nested type.
51 var builderType = builder.IntroduceClass(
52 "Builder",
53 buildType: t => t.Accessibility = Accessibility.Public );
54
55 // Add builder properties and update the mapping.
56 foreach ( var property in properties )
57 {
58 property.BuilderProperty =
59 builderType.IntroduceAutomaticProperty(
60 property.SourceProperty.Name,
61 property.SourceProperty.Type,
62 IntroductionScope.Instance )
63 .Declaration;
64 }
65
66 // Add a builder constructor accepting the required properties and update the mapping.
67 if ( properties.Any( m => m.IsRequired ) )
68 {
69 builderType.IntroduceConstructor(
70 nameof(this.BuilderConstructorTemplate),
71 buildConstructor: c =>
72 {
73 foreach ( var property in properties.Where( m => m.IsRequired ) )
74 {
75 property.BuilderConstructorParameterIndex = c.AddParameter(
76 property.SourceProperty.Name,
77 property.SourceProperty.Type )
78 .Index;
79 }
80 } );
81 }
82
83 // Add a Build method to the builder.
84 builderType.IntroduceMethod(
85 nameof(this.BuildMethodTemplate),
86 IntroductionScope.Instance,
87 buildMethod: m =>
88 {
89 m.Name = "Build";
90 m.Accessibility = Accessibility.Public;
91 m.ReturnType = builder.Target;
92
93 foreach ( var property in properties )
94 {
95 property.BuilderConstructorParameterIndex =
96 m.AddParameter( property.SourceProperty.Name, property.SourceProperty.Type )
97 .Index;
98 }
99 } );
100
101 // Add a constructor to the source type with all properties.
102 var constructor = builder.IntroduceConstructor(
103 nameof(this.SourceConstructorTemplate),
104 buildConstructor: c =>
105 {
106 c.Accessibility = Accessibility.Private;
107
108 foreach ( var property in properties )
109 {
110 property.SourceConstructorParameterIndex = c.AddParameter(
111 property.SourceProperty.Name,
112 property.SourceProperty.Type )
113 .Index;
114 }
115 } )
116 .Declaration;
117
118 builder.Tags = new Tags( properties, constructor );
119 }
120
121 [Template]
122 private void BuilderConstructorTemplate()
123 {
124 var tags = (Tags) meta.Tags.Source!;
125
126 foreach ( var property in tags.Properties.Where( p => p.IsRequired ) )
127 {
128 property.BuilderProperty!.Value =
129 meta.Target.Parameters[property.BuilderConstructorParameterIndex!.Value].Value;
130 }
131 }
132
133 [Template]
134 private void SourceConstructorTemplate()
135 {
136 var tags = (Tags) meta.Tags.Source!;
137
138 foreach ( var property in tags.Properties )
139 {
140 property.SourceProperty!.Value =
141 meta.Target.Parameters[property.SourceConstructorParameterIndex!.Value].Value;
142 }
143 }
144
145 [Template]
146 private dynamic BuildMethodTemplate()
147 {
148 var tags = (Tags) meta.Tags.Source!;
149
150 return tags.SourceConstructor.Invoke( tags.Properties.Select( x => x.BuilderProperty! ) )!;
151 }
152}
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 { get; set; }
27
28 private string Name { get; set; }
29
30 public Material Build(string Name, double Density)
31 {
32 return new Material(this.Name, this.Density);
33 }
34 }
35}
Note
For more about the Builder pattern, see Implementing the Builder pattern without boilerplate.