Aspect Injector

repository·master·Indexed 21 days ago

https://github.com/pamidur/aspect-injector

A compile-time, attribute-based Aspect-Oriented Programming (AOP) framework for .NET. It modifies assemblies during compilation to inject cross-cutting concerns into methods, properties, constructors, and events with zero runtime overhead. Features include support for Before, After, and Around advice, mixins for interface implementation, and built-in attributes such as [MemoryCache], [Freezable], [Lazy], and [Notify] for common patterns like caching, lazy initialization, and property change notifications.

Tokens
7.8K
Snippets
29
Records
36
Agent score
74%

What's inside Aspect Injector

  1. What is Aspect Injector?

    master
    Aspect Injector is an attribute-based framework for creating and injecting aspects into your .NET assemblies. It performs compile-time injection, making it compatible with Blazor and AOT (Ahead-of-Time) compilation. It allows you to inject logic into Methods, Constructors, Properties, and Events using Before, After, and Around (wrap) advice. It also supports injecting into Interface implementations.
  2. What is an Advice Effect and how to define it

    master

    An Advice Effect is code that can be injected into a method, property, or event.

    To define advice, create a class (an aspect) containing methods marked with the [Advice] attribute. The attribute requires a Kind parameter to determine when the code executes relative to the target method.

    Advice Kinds

    • Kind.Before: Executed before the method begins. For constructors, this runs after the base class constructor but before the target constructor body.
    • Kind.After: Executed after the method ends.
    • Kind.Around: Executed instead of the target method. You can optionally call the original method within this advice. Note: Around advice must return an object that can be cast to the target method's return type. If the target returns void, the advice must return null. Around advice cannot be applied to constructors.
        [Advice(Kind.Before)]
        public void LogEnter() {
            Console.WriteLine("Entering ...");
        }
    
        [Advice(Kind.After)]
        public void LogExit() {
            Console.WriteLine("Leaving ...");
        }
    
        [Advice(Kind.Around)]
        public object LogAndMeasureTimings(...) {
            // ... implementation ...
        }
    }
  3. Use Advice Effect Arguments to access target information

    master

    Advice methods in Aspect Injector can accept parameters decorated with the [Argument] attribute to access metadata about the target being intercepted. This allows you to write dynamic logic based on the name, type, or instance of the target.

    Commonly used Source values for arguments include:

    • Source.Name: The name of the method, property, or event.
    • Source.Type: The Type containing the target method.
    • Source.Instance: The object instance owning the target method (will be null for static methods).
    • Source.Metadata: The reflection metadata of the target method.
    class LogAspect {
        [Advice(Kind.Before, Targets = Target.Method)]
        public void LogEnter([Argument(Source.Name)] string name)
        {
            Console.WriteLine($"Entering method '{name}'.");
        }
    }
  4. How Mixins work to add properties and logic to objects

    master

    Mixins allow you to add new logic or properties to a target object by automatically implementing interfaces. When an aspect contains a mixin, it creates members on the target object that match the interface's members and proxies them back to the aspect instance.

    To use a Mixin, follow these steps:

    1. Define an interface describing the features you want to add.
    2. Create an aspect that implements that interface and is decorated with the [Mixin] attribute pointing to the interface type.
    3. Apply the aspect to the target class or a specific member of the target class.

    Note: You do not need to apply the aspect to the class itself; applying it to any member of the class is sufficient to trigger the injection.

    // 1. Define the interface
    public interface IHaveProperty
    {
      string Data { get; set; }
    }
    
    // 2. Create the aspect implementing the interface
    [Aspect(Scope.Global)]
    [Injection(typeof(MyAspect))]
    [Mixin(typeof(IHaveProperty))]
    public class MyAspect : Attribute, IHaveProperty 
    {
      public string Data { get; set; }
    }
    
    // 3. Apply to the target
    [MyAspect]
    public class Target
    {
      public void Do() {}
    }
  5. Configure Aspect Scopes

    master

    Aspect Injector supports two scopes that determine the lifecycle and instantiation of your aspect:

    1. Scope.Global: The aspect operates as a singleton across the entire application.
    2. Scope.PerInstance: Every target class receives its own unique instance of the aspect. Even if the aspect is injected into multiple members within the same class, only one instance of the aspect is created for that specific class instance.
  6. How to inject aspects using triggers

    master

    In AspectInjector, injection is performed by applying a trigger attribute to a target (class, method, etc.).

    To implement this, you must follow two steps:

    1. Define an Aspect: Create a class and mark it with the [Aspect] attribute.
    2. Define a Trigger: Create a .NET attribute class and mark it with the [Injection] attribute, passing the type of the aspect you want to trigger.

    Note that a trigger is only an "attempt" to inject. The aspect itself decides whether to execute based on its internal logic (e.g., if an aspect is configured to only target public members, applying its trigger to a private member will result in no injection).

    // 1. Defining the aspect
    [Aspect(Scope.Global)]
    class LogAspect {}
    
    // 2. Defining the trigger
    [Injection(typeof(LogAspect))]
    class Log : Attribute {}
    
    // 3. Applying the trigger to a target
    class TestClass {
        [Log]
        public void DoSomething() {}
    }
  7. Understand Aspect Injector terminology

    master

    To use Aspect Injector effectively, you should understand its core abstractions which align with standard AOP (Aspect-Oriented Programming) concepts:

    • Aspect: A class that encapsulates specific logic. It contains one or more Effects.
    • Effect: A formal description of how an Aspect interacts with other classes. There are two types of effects:
      • Advice: Modifies a method (e.g., injecting code before, after, or instead of the original method).
      • Mixin: Modifies a class (e.g., altering the interfaces a class implements).
    • Trigger: A .NET attribute applied to a target to tell Aspect Injector which aspect should be injected.
    • Injection (or Pointcut): The process where an aspect consumer uses triggers to apply aspect logic to specific targets.
  8. How Aspect Injector works

    master

    AspectInjector is a compile-time Aspect-Oriented Programming (AOP) framework. Unlike runtime AOP frameworks that use reflection or proxies at execution time, AspectInjector modifies your assembly during compilation.

    This means the 'weaving' of aspects into your code happens before the application runs, resulting in zero performance overhead during execution. The framework injects direct calls to the aspect instances into your methods, making the cross-cutting concerns as efficient as manual code.

    // Before compilation (User Code):
    [Aspect(Scope.Global)]
    [Injection(typeof(Log))]
    class Log : Attribute
    {
        [Advice(Kind.Before, Targets = Target.Method)]
        public void OnEntry([Argument(Source.Name)] string name)
        {
            Console.WriteLine($"Entering method {name}");
        }
    }
    
    class TestClass
    {
        [Log]
        public void Do()
        {
            Console.WriteLine($"Done");
        }
    }
    
    // After compilation (What actually runs):
    [Aspect(Scope.Global)]
    [Injection(typeof(Log))]
    class Log : Attribute
    {
        public static readonly Log __a$_instance;
    
        [Advice(Kind.Before, Targets = Target.Method)]
        public void OnEntry([Argument(Source.Name)] string name)
        {
            Console.WriteLine($"Entering method {name}");
        }
    
        static Log()
        {
            __a$_instance = new Log();
        }
    }
    
    internal class TestClass
    {
        [Log]
        public void Do()
        {
            Log.__a$_instance.OnEntry("Do");
            Console.WriteLine($"Done");
        }
    }
  9. How injection propagation works

    master

    When a trigger is applied to a class, it propagates the injection to all submembers (methods, properties, etc.) of that class.

    For example, applying [Log] to TestClass is functionally equivalent to applying [Log] to every individual member within TestClass, or applying [assembly: Log] at the assembly level.

    // This propagates to all members
    [Log]
    class TestClass {
        public void DoSomething() {}
        public void DoSomethingElse() {}
    }
  10. Use a factory to instantiate Aspects

    master

    By default, aspects are created using a parameterless constructor. If you need custom instantiation logic, you can specify a factory class using the Factory property of the [Aspect] attribute.

    The factory class must contain a static method with the signature public static object GetInstance(Type type).

    [Aspect(Scope.Global, Factory = typeof(AspectFactory))]
    class Log
    {
    }
    
    class AspectFactory
    {
        public static object GetInstance(Type type)
        {
            // Implementation logic
        }
    }
  11. Implement the Freezable pattern using [Freezable]

    master

    You can prevent properties from being modified after a certain point by applying the [Freezable] attribute to either a specific property or an entire class.

    To manage the frozen state, cast the object to the IFreezable interface. This interface provides the Freeze() method to lock the object and allows you to check its status. Once an object is frozen, attempting to modify any decorated properties will throw an exception.

    [Freezable]
    class TestClass
    {
        public string Data { get; set; }
    }
    
    public void Code()
    {
        var target = new TestClass();
        target.Data = "test1";
        
        // Cast to IFreezable to trigger the freeze
        ((IFreezable)target).Freeze();
        
        // This will throw an exception because the object is frozen
        target.Data = "test2"; 
    }
  12. Implement lazy initialization with LazyAttribute

    master

    The LazyAttribute (referred to as [Lazy] in code) allows you to perform lazy initialization on properties. When applied to a read-only property, the aspect injector transforms the property so that the initialization logic is only executed once, and the result is cached.

    Under the hood, LazyAttribute uses a Dictionary to cache the results of the property access.

    class TestClass
    {
        [Lazy]
        public ServiceA ServiceA => new ServiceA(DateTime.Now);
    }