In Adding aspects to your code, you learned how to apply aspects individually using custom attributes. While this approach is suitable for aspects like caching or auto-retry, it can be cumbersome for other aspects such as logging or profiling.
In this article, you'll learn how to use fabrics to add aspects to your targets programmatically.
When to use fabrics
Fabrics let you add aspects from a central location. Use fabrics instead of custom attributes when you can express the decision to add an aspect as a rule that depends solely on the metadata of the declaration, such as its name, signature, parent type, implemented interfaces, custom attributes, or any other detail exposed by the code model.
For instance, to add logging to all public methods of all public types in a namespace, use a fabric.
Conversely, don't use a fabric to add caching to all methods that start with the word Get because you may create more problems than you solve. Caching is typically an aspect you'd carefully select, and custom attributes are a better approach.
Adding aspects using fabrics
To add aspects using fabrics:
Create a fabric class and derive it from ProjectFabric.
Override the AmendProject abstract method.
Call one of the following methods from AmendProject:
- To select all types in the project, use the amender.SelectTypes method.
- To select type members (methods, fields, nested types, etc.), call the SelectMany method and provide a lambda expression that selects the relevant type members (for example,
SelectMany( t => t.Methods )to select all methods). - To filter types or members, use the Where method.
Call the AddAspect or AddAspectIfEligible method.
Note
The amender object selects not only members declared in source code but also members introduced by other aspects and therefore unknown when the AmendType method executes. This is why these methods don't directly expose the code model.
Example 1: Adding aspect to all methods in a project
In the following example, we use a fabric to apply a logging aspect to all methods in the current project.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.ProjectFabric_;
5
6public class Log : OverrideMethodAspect
7{
8 public override dynamic? OverrideMethod()
9 {
10 Console.WriteLine( $"Executing {meta.Target.Method}." );
11
12 try
13 {
14 return meta.Proceed();
15 }
16 finally
17 {
18 Console.WriteLine( $"Exiting {meta.Target.Method}." );
19 }
20 }
21}
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Fabrics;
3using System.Linq;
4
5namespace Doc.ProjectFabric_;
6
7internal class Fabric : ProjectFabric
8{
9 // This method is the compile-time entry point of your project.
10 // It executes within the compiler or IDE.
11 public override void AmendProject( IProjectAmender project )
12 {
13 project.SelectMany( p => p.Types.SelectMany( t => t.Methods ) ).AddAspectIfEligible<Log>();
14 }
15}
1using System;
2
3namespace Doc.ProjectFabric_;
4
5internal class Class1
6{
7 public void Method1()
8 {
9 Console.WriteLine( "Inside Class1.Method1" );
10 }
11
12 public void Method2()
13 {
14 Console.WriteLine( "Inside Class1.Method2" );
15 }
16}
17
18internal class Class2
19{
20 public void Method1()
21 {
22 Console.WriteLine( "Inside Class2.Method1" );
23 }
24
25 public void Method2()
26 {
27 Console.WriteLine( "Inside Class2.Method2" );
28 }
29}
1using System;
2
3namespace Doc.ProjectFabric_;
4
5internal class Class1
6{
7 public void Method1()
8 {
9 Console.WriteLine("Executing Class1.Method1().");
10 try
11 {
12 Console.WriteLine("Inside Class1.Method1");
13 return;
14 }
15 finally
16 {
17 Console.WriteLine("Exiting Class1.Method1().");
18 }
19 }
20
21 public void Method2()
22 {
23 Console.WriteLine("Executing Class1.Method2().");
24 try
25 {
26 Console.WriteLine("Inside Class1.Method2");
27 return;
28 }
29 finally
30 {
31 Console.WriteLine("Exiting Class1.Method2().");
32 }
33 }
34}
35
36internal class Class2
37{
38 public void Method1()
39 {
40 Console.WriteLine("Executing Class2.Method1().");
41 try
42 {
43 Console.WriteLine("Inside Class2.Method1");
44 return;
45 }
46 finally
47 {
48 Console.WriteLine("Exiting Class2.Method1().");
49 }
50 }
51
52 public void Method2()
53 {
54 Console.WriteLine("Executing Class2.Method2().");
55 try
56 {
57 Console.WriteLine("Inside Class2.Method2");
58 return;
59 }
60 finally
61 {
62 Console.WriteLine("Exiting Class2.Method2().");
63 }
64 }
65}
The key method is AmendProject. This method adds aspects to different members of a project, essentially amending the project.
Inside the AmendProject method, we get all public methods and add logging and retrying aspects to them.
AddAspect or AddAspectIfEligible?
The difference between AddAspect and AddAspectIfEligible is that AddAspect throws an exception when you try adding an aspect to an ineligible target (for instance, a caching aspect to a void method), while AddAspectIfEligible silently ignores such targets.
- If you use AddAspect, you may need to add many conditions to your
AmendProjectmethod to avoid exceptions. The benefit is you'll be aware of these conditions. - If you use AddAspectIfEligible, some target declarations may be silently ignored.
In most cases, we recommend using AddAspectIfEligible.
Example 2: Adding more aspects using the same Fabric
In the following example, we add two aspects: logging and profiling. We add profiling only to public methods of public classes.
For each project, we recommend one project fabric. Multiple project fabrics make it difficult to understand the aspect application order.
1using Metalama.Framework.Aspects;
2using System;
3using System.Diagnostics;
4
5namespace Doc.ProjectFabric_TwoAspects;
6
7public class Log : OverrideMethodAspect
8{
9 public override dynamic? OverrideMethod()
10 {
11 Console.WriteLine( $"Executing {meta.Target.Method}." );
12
13 try
14 {
15 return meta.Proceed();
16 }
17 finally
18 {
19 Console.WriteLine( $"Exiting {meta.Target.Method}." );
20 }
21 }
22}
23
24public class Profile : OverrideMethodAspect
25{
26 public override dynamic? OverrideMethod()
27 {
28 var stopwatch = Stopwatch.StartNew();
29
30 try
31 {
32 return meta.Proceed();
33 }
34 finally
35 {
36 Console.WriteLine(
37 $"{meta.Target.Method} completed in {stopwatch.ElapsedMilliseconds}." );
38 }
39 }
40}
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.Linq;
5
6namespace Doc.ProjectFabric_TwoAspects;
7
8internal class Fabric : ProjectFabric
9{
10 // This method is the compile-time entry point of your project.
11 // It executes within the compiler or IDE.
12 public override void AmendProject( IProjectAmender project )
13 {
14 AddLogging( project );
15 AddProfiling( project );
16 }
17
18 private static void AddLogging( IProjectAmender project )
19 {
20 project
21 .SelectMany( p => p.Types )
22 .SelectMany( t => t.Methods )
23 .AddAspectIfEligible<Log>();
24 }
25
26 private static void AddProfiling( IProjectAmender project )
27 {
28 project
29 .SelectMany( p => p.Types.Where( t => t.Accessibility == Accessibility.Public ) )
30 .SelectMany( t => t.Methods.Where( m => m.Accessibility == Accessibility.Public ) )
31 .AddAspectIfEligible<Profile>();
32 }
33}
1using System;
2
3namespace Doc.ProjectFabric_TwoAspects;
4
5internal class Class1
6{
7 public void Method1()
8 {
9 Console.WriteLine( "Inside Class1.Method1" );
10 }
11
12 public void Method2()
13 {
14 Console.WriteLine( "Inside Class1.Method2" );
15 }
16}
17
18public class Class2
19{
20 public void Method1()
21 {
22 Console.WriteLine( "Inside Class2.Method1" );
23 }
24
25 public void Method2()
26 {
27 Console.WriteLine( "Inside Class2.Method2" );
28 }
29}
1using System;
2using System.Diagnostics;
3
4namespace Doc.ProjectFabric_TwoAspects;
5
6internal class Class1
7{
8 public void Method1()
9 {
10 Console.WriteLine("Executing Class1.Method1().");
11 try
12 {
13 Console.WriteLine("Inside Class1.Method1");
14 return;
15 }
16 finally
17 {
18 Console.WriteLine("Exiting Class1.Method1().");
19 }
20 }
21
22 public void Method2()
23 {
24 Console.WriteLine("Executing Class1.Method2().");
25 try
26 {
27 Console.WriteLine("Inside Class1.Method2");
28 return;
29 }
30 finally
31 {
32 Console.WriteLine("Exiting Class1.Method2().");
33 }
34 }
35}
36
37public class Class2
38{
39 public void Method1()
40 {
41 Console.WriteLine("Executing Class2.Method1().");
42 try
43 {
44 var stopwatch = Stopwatch.StartNew();
45 try
46 {
47 Console.WriteLine("Inside Class2.Method1");
48 }
49 finally
50 {
51 Console.WriteLine($"Class2.Method1() completed in {stopwatch.ElapsedMilliseconds}.");
52 }
53
54 return;
55 }
56 finally
57 {
58 Console.WriteLine("Exiting Class2.Method1().");
59 }
60 }
61
62 public void Method2()
63 {
64 Console.WriteLine("Executing Class2.Method2().");
65 try
66 {
67 var stopwatch = Stopwatch.StartNew();
68 try
69 {
70 Console.WriteLine("Inside Class2.Method2");
71 }
72 finally
73 {
74 Console.WriteLine($"Class2.Method2() completed in {stopwatch.ElapsedMilliseconds}.");
75 }
76
77 return;
78 }
79 finally
80 {
81 Console.WriteLine("Exiting Class2.Method2().");
82 }
83 }
84}
Example 3: Adding aspects to all methods in a namespace using a NamespaceFabric
To add aspects to all methods within a specific namespace, use a NamespaceFabric. Unlike a ProjectFabric, which operates at the project level, a NamespaceFabric is placed directly within the target namespace and automatically scopes its operations to that namespace.
1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.NamespaceFabric_.TargetNamespace;
5
6public class Log : OverrideMethodAspect
7{
8 public override dynamic? OverrideMethod()
9 {
10 Console.WriteLine( $"Entering {meta.Target.Method}." );
11
12 try
13 {
14 return meta.Proceed();
15 }
16 finally
17 {
18 Console.WriteLine( $"Leaving {meta.Target.Method}." );
19 }
20 }
21}
22
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Fabrics;
3
4namespace Doc.NamespaceFabric_.TargetNamespace;
5
6// A namespace fabric is placed directly in the namespace it affects.
7// It applies to all types within that namespace and its descendants.
8internal class Fabric : NamespaceFabric
9{
10 public override void AmendNamespace( INamespaceAmender amender )
11 {
12 // Add the Log aspect to all methods in this namespace.
13 amender
14 .SelectTypes()
15 .SelectMany( t => t.Methods )
16 .AddAspectIfEligible<Log>();
17 }
18}
19
1using System;
2
3namespace Doc.NamespaceFabric_
4{
5 namespace TargetNamespace
6 {
7 internal class TargetClass
8 {
9 public void TargetMethod()
10 {
11 Console.WriteLine( "Executing TargetMethod" );
12 }
13 }
14 }
15
16 namespace OtherNamespace
17 {
18 internal class OtherClass
19 {
20 public void OtherMethod()
21 {
22 Console.WriteLine( "Executing OtherMethod" );
23 }
24 }
25 }
26}
27
1using System;
2
3namespace Doc.NamespaceFabric_
4{
5 namespace TargetNamespace
6 {
7 internal class TargetClass
8 {
9 public void TargetMethod()
10 {
11 Console.WriteLine("Entering TargetClass.TargetMethod().");
12 try
13 {
14 Console.WriteLine("Executing TargetMethod");
15 return;
16 }
17 finally
18 {
19 Console.WriteLine("Leaving TargetClass.TargetMethod().");
20 }
21 }
22 }
23 }
24
25 namespace OtherNamespace
26 {
27 internal class OtherClass
28 {
29 public void OtherMethod()
30 {
31 Console.WriteLine("Executing OtherMethod");
32 }
33 }
34 }
35}
36
In this example, the Fabric class inherits from NamespaceFabric and is placed inside TargetNamespace. The fabric's AmendNamespace method uses SelectTypes() to get all types within the namespace, then SelectMany( t => t.Methods ) to select their methods, and adds the Log aspect to all eligible methods.
Notice how TargetMethod in TargetNamespace gets the logging aspect applied (shown in the transformed code), while OtherMethod in OtherNamespace remains unchanged because the fabric only affects its containing namespace.
Performance: Avoid filtering all types by namespace
A common mistake when adding aspects to a specific namespace from a ProjectFabric is to enumerate all types and filter by namespace:
// DON'T do this - it enumerates ALL types in the project, which is slow
amender.SelectTypes()
.Where( t => t.Namespace.FullName.StartsWith( "MyNamespace" ) )
.AddAspectIfEligible<LogAttribute>();
This approach is inefficient because it enumerates every type in the entire project.
Instead, use one of these approaches:
Option 1: Use a NamespaceFabric (recommended)
Place a NamespaceFabric directly in the target namespace, as shown in Example 3 above. This is the most readable approach.
Option 2: Use GlobalNamespace.GetDescendant
If you need to target a namespace from a ProjectFabric, navigate directly to the namespace using GetDescendant:
// DO this - navigates directly to the namespace
amender.Select( c => c.GlobalNamespace.GetDescendant( "MyNamespace" )! )
.SelectTypes()
.SelectMany( t => t.Methods )
.AddAspectIfEligible<LogAttribute>();
Note
SelectTypes selects types in the specified namespace only, not in child namespaces. To include types in child namespaces, use DescendantsAndSelf:
amender.Select( c => c.GlobalNamespace.GetDescendant( "MyNamespace" )! )
.SelectMany( ns => ns.DescendantsAndSelf() )
.SelectTypes()
.SelectMany( t => t.Methods )
.AddAspectIfEligible<LogAttribute>();
Example 4: Adding the Log aspect only to derived classes of a given class
Sometimes you may want to add aspects only to a class and its derived types. The following fabric shows how to accomplish this by getting the derived types of a given type and adding aspects to them.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4
5namespace Metalama.Documentation.QuickStart.Fabrics
6{
7 internal class AddLoggingToBaseClassChildren : ProjectFabric
8 {
9 public override void AmendProject( IProjectAmender amender )
10 {
11 amender
12
13 //Locate all derived types of a given base class
14 .SelectTypesDerivedFrom( typeof(BaseClass) )
15
16 //Find all methods of all of these types
17 .SelectMany( t => t.Methods )
18
19 //Find all the public member functions of these types
20 .Where( method => method.Accessibility == Accessibility.Public )
21
22 //Add `Log` attribute to all of these
23 .AddAspectIfEligible<LogAttribute>();
24 }
25 }
26}
Tip
Use code metrics to filter declarations based on complexity. For example, add logging only to methods exceeding a certain number of syntax nodes. For details, see Consuming code metrics.