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.

  • @RonSijm
    link
    3
    edit-2
    4 months ago

    The current answers using a Func or an Action are correctish, but with that approach you will have logging cross-cutting hard-wired into all your classes before invoking the methods.

    If you want true AOP or decorators, you probably want to use Type Interceptors. And Castle DynamicProxy. See: https://autofac.readthedocs.io/en/latest/advanced/interceptors.html https://blog.zhaytam.com/2020/08/18/aspnetcore-dynamic-proxies-for-aop/

    A dynamic proxy will give you a method like this:

    public void Intercept(IInvocation invocation)
    {
      _output.Write("Calling method {0} with parameters {1}... ",
        invocation.Method.Name,
        string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));
    
      invocation.Proceed();
    
      _output.WriteLine("Done: result was {0}.", invocation.ReturnValue);
    }
    

    Intercept is called before invoking a method, you can do stuff before the method starts, then you call invocation.Proceed(); - that invokes the method, and then you can log the result.

    Note that your class methods either need to be virtual, or it needs to have an interface, I’d suggest an interface.

    This way you can wire the logging part in through dependency injection in your container, and your classes itself don’t contain logging cross-cutting logic


    Logging wise, I’m not sure how familiar you are with dotnet, as you’re saying you’re just starting, I’d suggest inside your classes you stick with Microsoft.Extensions.Logging.Abstractions - so your classes - lets say a class MyService - would be

    public class MyService(Microsoft.Extensions.Logging.ILogger<MyService> logger) { }

    Then I’d suggest Serilog to use as concrete logger. You’d wire that in with:

         var configuration = new ConfigurationBuilder()
            .SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!)
            .AddJsonFile("appsettings.json")
            .Build();
    
        services.AddLogging(loggingBuilder =>
        {
            var logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .CreateLogger();
    
            loggingBuilder.AddSerilog(logger, dispose: true);
        });
    

    Sirilog is a very extendable framework that comes with 100s of different Sinks so you can very easily wire in a different output target, or wire in different enrichers without modifying any of your existing code

    • @pips34OP
      link
      14 months ago

      Beautiful! I only reply just now because I JUST got it working. This is exactly what I was looking for, thank you sooo much :) I’m keeping both approaches in a lil template project I have, and definitely checking Serilog next!