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 will learn how to use fabrics to add aspects to your targets programmatically.
When to use fabrics
Fabrics allow you to add all aspects from a central location. Consider using fabrics instead of custom attributes when the decision to add an aspect to a declaration can be easily expressed as a rule, and when this rule 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, if you want to add logging to all public methods of all public types of a namespace, it is more efficient to do it using a fabric.
Conversely, it may not be advisable to use a fabric to add caching to all methods that start with the word Get because you may end up creating more problems than you solve. Caching is typically an aspect you would 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, e.g.,
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 will not only select members declared in source code, but also members introduced by other aspects and therefore unknown when the AmendType method is executed. This is why these methods do not 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.Fabrics;
2using System.Linq;
3
4namespace Doc.ProjectFabric_;
5
6internal class Fabric : ProjectFabric
7{
8 // This method is the compile-time entry point of your project.
9 // It executes within the compiler or IDE.
10 public override void AmendProject( IProjectAmender project )
11 {
12 project.SelectMany( p => p.Types.SelectMany( t => t.Methods ) ).AddAspectIfEligible<Log>();
13 }
14}
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}
There are a few things to note in this example. The first point to consider is the AmendProject
method. We aim to add aspects to different members of a project. Essentially, we are trying to amend the project, hence the name.
Inside the AmendProject
method, we get all the public methods and add logging and retrying aspects to these methods.
Warning
Sometimes CodeLense misses the aspects to show. For that time, it is required to rebuild the project.
AddAspect or AddAspectIfEligible?
The difference between AddAspect and AddAspectIfEligible is that AddAspect will throw an exception if you try adding an aspect to an ineligible target (for instance, a caching aspect to a void
method), while AddAspectIfEligible will silently ignore such targets.
- If you choose AddAspect, you may be annoyed by exceptions and may have to add a lot of conditions to your
AmendProject
method. The benefit of this approach is that you will be aware of these conditions. - If you choose AddAspectIfEligible, you may be surprised that some target declarations were silently ignored.
As is often the case, life does not give you a choice to be completely happy, but you can often choose which pain you want to suffer. 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, it is recommended to have only one project fabric. Having several project fabrics makes 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.Code;
2using Metalama.Framework.Fabrics;
3using System.Linq;
4
5namespace Doc.ProjectFabric_TwoAspects;
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 AddLogging( project );
14 AddProfiling( project );
15 }
16
17 private static void AddLogging( IProjectAmender project )
18 {
19 project
20 .SelectMany( p => p.Types )
21 .SelectMany( t => t.Methods )
22 .AddAspectIfEligible<Log>();
23 }
24
25 private static void AddProfiling( IProjectAmender project )
26 {
27 project
28 .SelectMany( p => p.Types.Where( t => t.Accessibility == Accessibility.Public ) )
29 .SelectMany( t => t.Methods.Where( m => m.Accessibility == Accessibility.Public ) )
30 .AddAspectIfEligible<Profile>();
31 }
32}
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 given namespace
To add the Logging aspect (LogAttribute
) to all the methods that appear in types within namespaces that start with the prefix Outer.Inner
and all the child types located in any descendant namespace, use the following fabric.
1using Metalama.Documentation.QuickStart;
2using Metalama.Framework.Fabrics;
3
4namespace DebugDemo2
5{
6 public class AddLogAspectInGivenNamespaceFabric : ProjectFabric
7 {
8 /// <summary>
9 /// Amends the project by adding Log aspect
10 /// to many eligible methods inside given namespace.
11 /// </summary>
12 public override void AmendProject( IProjectAmender amender )
13 {
14 //Adding Log attribute to all mehtods of all types
15 //that are available inside "Outer.Inner" namespace
16
17 amender
18 .SelectTypes()
19 .SelectMany( t => t.Methods )
20 .AddAspectIfEligible<LogAttribute>();
21 }
22 }
23}
In this fabric, we use the GlobalNamespace.GetDescendant
method to retrieve all child namespaces of the given namespace (in this case, Outer.Inner
). The first SelectMany
call retrieves all the types in these namespaces, and the subsequent SelectMany
call retrieves all the methods in these types. This results in an IAspectReceiver<IMethod>
. The final call to AddAspectIfEligible adds the Log
aspect to all eligible methods.
Example 4: Adding the Log
aspect only to derived classes of a given class
Sometimes you may not need or want to add aspects to all types, but only to a class and its derived types. The following fabric shows how you can accomplish this. In this example fabric, you will see how to get the derived types of a given type and how to add aspects to them.
1using Metalama.Framework.Code;
2using Metalama.Framework.Fabrics;
3
4namespace Metalama.Documentation.QuickStart.Fabrics
5{
6 internal class AddLoggingToBaseClassChildren : ProjectFabric
7 {
8 public override void AmendProject( IProjectAmender amender )
9 {
10 amender
11
12 //Locate all derived types of a given base class
13 .SelectTypesDerivedFrom( typeof(BaseClass) )
14
15 //Find all methods of all of these types
16 .SelectMany( t => t.Methods )
17
18 //Find all the public member functions of these types
19 .Where( method => method.Accessibility == Accessibility.Public )
20
21 //Add `Log` attribute to all of these
22 .AddAspectIfEligible<LogAttribute>();
23 }
24 }
25}