Hello! I’m starting a personal project in .NET.

For a logging solution, I want to achieve something similar to what I have in a Python REST API I wrote a while back using the decorator pattern (example in the image).

In the example, the outter “log” function receives the logger I want to use, the decorator function receives a function and returns the decorated function ready to be called (this would be the decorator), and the wrapper function receives the same arguments as the function to be decorated (which is a generic (*args, **kwargs) ofc, because it’s meant to decorate any function) and returns whatever the return type of the function is (so, Any). In lines 17 - 24 I just call the passed in “func” with the passed in arguments, but in between I wrap it in a try except block and log that the function with name func.__name__ started or finished executing. In practice, using this decorator in Python looks like this:

import logging
from my.decorator.module import log

_logger = logging.getLogger(__name__)

@log(_logger)
def my_func(arg1: Arg1Type, arg2: Arg2Type) -> ReturnType:
    ...

Ofc it’s a lot simpler in Python, however I was wondering if it would be possible or even recommended to attempt something similar in C#. I wouldn’t mind having to call the function and wrap it manually, something like this:

return RunWithLogs(MyFunction, arg1, arg2);

What I do want to avoid is manually writing the log statements inside the service’s business logic, or having to write separate wrappers for each method I want to log. Would be nice to have one generic function or class that I can somehow plug-in to any method and have it log when the call starts and finishes.

Any suggestions? Thanks in advance.

  • theit8514@lemmy.world
    link
    fedilink
    arrow-up
    1
    ·
    5 months ago

    C# does not really implement decorators like that, which can execute autonomously. You would still need to wrap all the functions you want to call.

    You can wrap the action using a Func<> or Action, and get the calling member name (e.g. name would be ‘Main’). The Arguments are a pain, as you have to drill down the Expression tree to get them. This case only handles ConstantExpression values, you would also need to handle MemberExpression and probably others.

    using System.Linq.Expressions;
    using System.Runtime.CompilerServices;
    
        public static void Main()
        {
            var n = LogWrapper(() => MyMethod("Hello, World!"));
            Console.WriteLine(n);
        }
    
        private static int MyMethod(string test)
        {
            Console.WriteLine(test);
            return 42;
        }
    
        private static T LogWrapper(Expression> func, [CallerMemberName] string name = null)
        {
            // Do begin logging
            var body = func.Body as MethodCallExpression;
            var firstArgument = body?.Arguments.First() as ConstantExpression;
    
            Console.WriteLine("name={0} param={1}", name, firstArgument?.Value);
    
            var result = func.Compile().Invoke();
    
            // Do end logging
            Console.WriteLine("end name={0}", name);
            return result;
        }
    
    • Lmaydev@programming.dev
      link
      fedilink
      arrow-up
      2
      ·
      5 months ago

      Won’t you need to handle all the different expressions the parameter could be with this method? It won’t always be a constant right?

      • theit8514@lemmy.world
        link
        fedilink
        arrow-up
        2
        ·
        5 months ago

        Yes, as I stated other expression types might be needed. You could also wrap the argument expression in Expression.Lambda() and execute it to get the value. None of these options are necessarily fast, and you’ll likely get double executions which could lead to unanticipated side effects.

        • Lmaydev@programming.dev
          link
          fedilink
          arrow-up
          1
          ·
          5 months ago

          I think taking the parameters as parameters and creating a function for each parameter count is better then you can just call the func/action normally.