Open sandboxFocusImprove this doc

Adding many aspects simultaneously

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:

  1. Create a fabric class and derive it from ProjectFabric.

  2. Override the AmendProject abstract method.

  3. 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.
  4. 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}
Source Code
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}
Transformed Code
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}
Source Code
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}
Transformed Code
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}