14

In Java, it is possible to use AspectJ for adding behavior before and after executing a method, using method annotations. Since C# Attributes seem to be very similar, I was wondering whether it would be possible to achieve similar behavior. I was looking in several tutorials and other sources (1, 2, 3), but none of them helped me.

I thought that maybe I could be able to mimic the behavior by inserting the code into Attribute constructor and making it disposable, like this:

[AttributeUsage(AttributeTargets.Method)]
public class MyWritingAttribute : Attribute, IDisposable
{
    public MyWritingAttribute()
    {
        Console.WriteLine("Attribute created");
    }

    public void Dispose()
    {
        Console.WriteLine("Attribute disposed");
    }
}

However, when using the attribute like this, only Hello world! got displayed in the console:

class Program
{
    static void Main(string[] args)
    {
        SayHelloWorld();
        Console.ReadLine();
    }

    [MyWriting]
    private static void SayHelloWorld()
    {
        Console.WriteLine("Hello World!");
    }
}

I was thinking that maybe Console is not reachable in the attribute, but even when replacing it with throw new Exception() expressions, no exception was thrown. How is it possible that StringLengthAttribute from EF works, but my attribute is not even instantiated? And how do I make the attribute run before and after the decorated method?

lss
  • 1,235
  • 3
  • 15
  • 24
  • 3
    You have to use an AOP-Framework. Attributes only extend the metainformation of types. – Jehof Sep 01 '17 at 12:58
  • 5
    Attributes are metadata. They do not run at all. AspectJ works by rewriting of the compiled code after the fact -- the method annotations themselves do nothing. The best-known .NET equivalent is [PostSharp](https://www.postsharp.net/). – Jeroen Mostert Sep 01 '17 at 12:59
  • You need some framework that *reads* and handles the attribute. Just having it there won´t do anything. – MakePeaceGreatAgain Sep 01 '17 at 13:11

4 Answers4

12

You need some framework that is able to handle your attribute appropriately. Only because the attribute exists doesn´t mean it will have any effect.

I wrote some easy engine that does that. It will determine if the attribute is present on the passed action and if so get the reflected methods in order to execute them.

class Engine
{
    public void Execute(Action action)
    {
        var attr = action.Method.GetCustomAttributes(typeof(MyAttribute), true).First() as MyAttribute;
        var method1 = action.Target.GetType().GetMethod(attr.PreAction);
        var method2 = action.Target.GetType().GetMethod(attr.PostAction);

        // now first invoke the pre-action method
        method1.Invoke(null, null);
        // the actual action
        action();
        // the post-action
        method2.Invoke(null, null);
    }
}
public class MyAttribute : Attribute
{
    public string PreAction;
    public string PostAction;
}

Of course you need some null-checks, e.g. in case the methods don´t exist or aren´t static.

Now you have to decorate your action with the attribute:

class MyClass
{
    [MyAttribute(PreAction = "Handler1", PostAction = "Handler2")]
    public void DoSomething()
    {
        
    }

    public static void Handler1()
    {
        Console.WriteLine("Pre");
    }
    public static void Handler2()
    {
        Console.WriteLine("Post");
    }
}

Finally you can execute that method within our engine:

var engine = new Engine();
var m = new MyClass();
engine.Execute(m.DoSomething);
vandre
  • 778
  • 6
  • 16
MakePeaceGreatAgain
  • 35,491
  • 6
  • 60
  • 111
  • 5
    Because `nameof` produces compile-time constants, in C# 6 you can actually use `[MyAttribute(PreAction = nameof(Handler1), PostAction = nameof(Handler2))]`, with obvious benefits. – Jeroen Mostert Sep 01 '17 at 13:46
  • 1
    @JeroenMostert What a great suggestion, I already found it annoying to rely on reflection whle I posted this answer. It will greatly improve stability. – MakePeaceGreatAgain Sep 01 '17 at 13:47
7

Just like with Java and AspectJ, you need separate AoP tooling to inject code like this in .NET.

PostSharp is one such tool, probably the best known. I belive they have support for .NET core since version 5.

Anders Forsgren
  • 10,827
  • 4
  • 40
  • 77
  • Just leaving a link here to another answer I found useful: https://stackoverflow.com/a/60133882/3873799. This recommends Fody over PostSharp, due to its free nature (voluntary contribution) and flexibility. – alelom Apr 08 '23 at 14:54
5

This can be accomplished using DynamicProxy.

There is an implementation of a memory caching technique with logic that executes before the method being called. That can be extended to check for the existence of an attribute like this

var attribute = Attribute.GetCustomAttribute(invocation.MethodInvocationTarget, typeof(CachedAttribute)) as CachedAttribute;
if (attribute != null)
{
  ...
}

The code above can be inside the Intercept method in the Interceptor implementation. CachedAttribute would be your attribute.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Carlos Blanco
  • 8,592
  • 17
  • 71
  • 101
4

The question is similar to Run a method before all methods of a class, hence the same answer applies to both. Use https://github.com/Fody/Fody . The licencing model is based on voluntary contributions making it the better option to PostSharp which is a bit expensive for my taste.

[module: Interceptor]
namespace GenericLogging
{

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]
    public class InterceptorAttribute : Attribute, IMethodDecorator
    {
        // instance, method and args can be captured here and stored in attribute instance fields
        // for future usage in OnEntry/OnExit/OnException
        public void Init(object instance, MethodBase method, object[] args)
        {
            Console.WriteLine(string.Format("Init: {0} [{1}]", method.DeclaringType.FullName + "." + method.Name, args.Length));
        }

        public void OnEntry()
        {
            Console.WriteLine("OnEntry");
        }

        public void OnExit()
        {
            Console.WriteLine("OnExit");
        }

        public void OnException(Exception exception)
        {
            Console.WriteLine(string.Format("OnException: {0}: {1}", exception.GetType(), exception.Message));
        }
    }

    public class Sample
    {
        [Interceptor]
        public void Method(int test)
        {
            Console.WriteLine("Your Code");
        }
    }
}

[TestMethod]
public void TestMethod2()
{
    Sample t = new Sample();
    t.Method(1);
}
Nigel Findlater
  • 1,684
  • 14
  • 34