Open sandboxFocusImprove this doc

Adding aspects to multiple projects

If you manage a repository or solution with multiple projects, you may want to add aspects from a central location. This article outlines several methods to achieve this.

These approaches are also applicable when configuring aspect libraries or adding architectural rules.

Using transitive project fabrics

Transitive project fabrics are executed in any project that references the assembly containing the fabric, either as a project or package reference.

Execution order of transitive fabrics

Transitive project fabrics are executed after any project fabric in the current project.

If multiple transitive project fabrics are active, they are executed in the following order:

  1. Depth in the dependency graph: dependencies with lower depth (i.e., "closer" to the main project) are processed first.
  2. Assembly name (in alphabetical order).

Transitive dependencies are intentionally executed after compilation dependencies, allowing the latter to configure transitive dependencies before they run.

Example: Central logging policy

The following example shows a transitive project fabric that adds logging to all public methods. Any project that references the assembly containing this fabric automatically gets the logging aspect applied.

Source Code
1using System;
2
3namespace Doc.TransitiveFabric;
4
5// This project references the assembly containing LoggingPolicyFabric.
6// The transitive fabric automatically applies logging to all public methods.
7
8public class OrderService
9{
10    public void PlaceOrder( string orderId )
11    {
12        Console.WriteLine( $"Processing order {orderId}." );
13    }








14

15    public void CancelOrder( string orderId )
16    {
17        Console.WriteLine( $"Cancelling order {orderId}." );
18    }








19

20    private void ValidateOrder( string orderId )
21    {
22        // Private methods are not logged.
23    }
24}
25
Transformed Code
1using System;
2
3namespace Doc.TransitiveFabric;
4
5// This project references the assembly containing LoggingPolicyFabric.
6// The transitive fabric automatically applies logging to all public methods.
7
8public class OrderService
9{
10    public void PlaceOrder(string orderId)
11    {
12        Console.WriteLine("Entering OrderService.PlaceOrder(string)");
13        try
14        {
15            Console.WriteLine($"Processing order {orderId}.");
16            return;
17        }
18        finally
19        {
20            Console.WriteLine("Leaving OrderService.PlaceOrder(string)");
21        }
22    }
23
24    public void CancelOrder(string orderId)
25    {
26        Console.WriteLine("Entering OrderService.CancelOrder(string)");
27        try
28        {
29            Console.WriteLine($"Cancelling order {orderId}.");
30            return;
31        }
32        finally
33        {
34            Console.WriteLine("Leaving OrderService.CancelOrder(string)");
35        }
36    }
37
38    private void ValidateOrder(string orderId)
39    {
40        // Private methods are not logged.
41    }
42}
43
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Fabrics;
using System;

namespace Doc.TransitiveFabric;

// This transitive fabric applies logging to all public methods in any project
// that references this assembly.
public class LoggingPolicyFabric : TransitiveProjectFabric
{
    public override void AmendProject( IProjectAmender amender )
    {
        amender
            .SelectTypes()
            .Where( t => t.Accessibility == Accessibility.Public )
            .SelectMany( t => t.Methods )
            .Where( m => m.Accessibility == Accessibility.Public && m.Name != "ToString" )
            .AddAspectIfEligible();
    }
}

public class LogAttribute : OverrideMethodAspect
{
    public override dynamic? OverrideMethod()
    {
        Console.WriteLine( $"Entering {meta.Target.Method}" );

        try
        {
            return meta.Proceed();
        }
        finally
        {
            Console.WriteLine( $"Leaving {meta.Target.Method}" );
        }
    }
}

Dependency graph example

Consider the following dependency graph:

flowchart BT
    subgraph MySolution.Core
        CoreTransitiveFabric
        CoreClasses[Other types]
    end
    subgraph MySolution.Library1
        Library1Classes[Other types]
    end
    MySolution.Library1 --> MySolution.Core
    subgraph MySolution.Library2
        Library2TransitiveFabric
        Library2Classes[Other types]
    end
    MySolution.Library2 --> MySolution.Core
    subgraph MySolution.App
       AppClasses[Other types]
    end
    MySolution.App --> MySolution.Library1
    MySolution.App --> MySolution.Library2

In MySolution, the following transitive project fabrics will be active:

Project Active transitive project fabrics
MySolution.Core None
MySolution.Library1 CoreTransitiveFabric
MySolution.Library2 CoreTransitiveFabric
MySolution.App First CoreTransitiveFabric, then Library2TransitiveFabric

Using common project fabrics

Another approach is to rely on the directory structure instead of the dependency graph.

The concept is to write a project fabric, store it in the root directory of the repository, and automatically include this file in each project using Directory.Build.props.

Step 1. Create a project fabric

In the parent directory that recursively contains all projects you want to be affected by the shared fabric, create a project fabric derived from ProjectFabric as you would do for a regular project fabric.

Step 2. Create Directory.Build.props

In the same directory, create a file named Directory.Build.props with the following content:

<Project>
 <!-- Imports Directory.Build.props of the upper directory. -->
 <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
   Condition="Exists('$([MSBuild]::GetPathOfFileAbove(`Directory.Build.props`, `$(MSBuildThisFileDirectory)../`))')"/>

 <!-- Include the shared fabric -->
 <ItemGroup>
        <Compile Include="$(MSBuildThisFileDirectory)SharedFabric.cs" />
    </ItemGroup>
</Project>

Example

See Example: shared fabrics.

Execution order of shared fabrics

When you have multiple project fabrics in the same project, they're ordered by these criteria:

  1. Distance of the source file from the root directory: fabrics closer to the root directory are processed first.
  2. Fabric namespace.
  3. Fabric type name.

Example

Suppose we have the following project structure:


repo
+--- dir1
|     +-- subdir11
|     |   +-- Project11.csproj
|     |   +-- Project11Fabric.cs
|     +-- subdir12
|         + Project12.csproj
+--- dir2
|    +-- subdir21
|    |    +-- Project21.csproj
|    +-- subdir22
|        +-- Project22.csproj
|        +-- Project21Fabric.cs
+-- SharedFabric.cs
+-- Directory.Build.props

Then the projects have the following fabrics:

Project Active transitive project fabrics
Project11 SharedFabric, Project11Fabric
Project12 SharedFabric
Project21 SharedFabric
Project22 SharedFabric, Project21Fabric