0

Preamble/Context

So currently I have a Log method.

public T Log<T>(Func<T> func)
{
    string methodName = func.Method.Name;
    bool success = false;
    T product = default(T);

    Debug($"Entering {methodName}");

    try
    {
        product = func.Invoke();
        success = true;
    }
    catch (Exception e)
    {
        Info($"FAILURE: {methodName}: {e.Message}");
    }

    Debug($"{success.ToString().ToUpper()}: Exiting {methodName}");
    return product;
}

Which works really well for me and is called like this

Log(() => AddTwoNumbers(1, 2));

I've been using the decorator pattern and basically saying that all the methods of the interface call their base (or the contained object or however you want to go about implementing the base method) so that the methods are logged.

DISCLAIMER I don't have easy access to AOP in my situation so generating a proxy that handles this isn't an easy route. I'm mostly overhauling legacy code and making calls to my .NET libraries from Visual Basic 6 EXEs and from what I can see most of these AOP frameworks need to be incorporated directly into the app.

The Problem I'm Trying to Solve

Many times I find myself doing this:

public int AddFirstFourNumbers(params int[] numbahs) 
{
  Log(() => 
  {
    int product;
    product += numbahs[0];
    product += numbahs[1];
    product += numbahs[2];
    product += numbahs[3];

    return product;
  }
}

Or decorating all members of an extension class like this

public override int AddTwoNumbers(int num1, int num2)
{
  return Log(() => base.AddTwoNumbers(num1, num2));
}

and I'd like to turn that into this:

[Log]
public int AddFirstFourNumbers(params int[] numbahs) 
{
    int product;
    product += numbahs[0];
    product += numbahs[1];
    product += numbahs[2];
    product += numbahs[3];

    return product;
}

and this

[Log]
public override int AddTwoNumbers(int num1, int num2)
{
  return base.AddTwoNumbers(num1, num2);
}

I'm not very familiar with creating custom Attributes but is there a way to treat the method decorated by this log attribute as a delegate and pass it to the Log method described in the first block? The examples I see on the Microsoft Docs don't seem to do anything close to what I'm hoping for.

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes

At the same time we have the MVC validation attributes which seem to provide tons of helpful functionality so this may yet be possible.

I'm a huge fan of Action and Func Wrappers that allow me to decorate my base methods and separate out my concerns. I'm just looking for a cleaner way of doing so. It would be nice to be able to just add [WithImpersonation] to the top of a method or [UndoOnFailure] and only display the core implementation within the actual method block.

Community
  • 1
  • 1
Imbaker1234
  • 115
  • 2
  • 10
  • 2
    The way the MVC attrributes works is, there is code in the framework that looks for those attributes and activates them. You would need to do the same here, so using an attribute doesn't really buy you much. –  Jan 09 '20 at 04:01
  • Yeah it looks like it. I could [add reflection] (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/accessing-attributes-by-using-reflection) but then I'd be trading calling for Log and Lambda-ing the body of my method for calling for the attributes at the start of every method. Which is pretty much what AOP offers without the code clutter. – Imbaker1234 Jan 09 '20 at 04:10
  • I'm writing an answer, an alternative you can consider. –  Jan 09 '20 at 04:10
  • Hey, I've created the lib exactly for this (and much broader) purpose. If your app is at least netstandard2.0 compatible you can use 2.x otherwise 1.x versions. https://github.com/pamidur/aspect-injector – pamidur Jan 29 '20 at 15:26

1 Answers1

3

First, I want to talk about the problem you'll have using the attribute approach. Assume you have a method AddFirstFourNumbers decorated with the Log attribute, as described in your question.

The attribute exists as metadata on the method. That's it. Any methods it contains will only be called if you specifically call them. In other words:

var attrib = typeof(ClassWithAddFirstFourNumbers)  // get the attribute
    .GetMethod(nameof(AddFirstFourNumbers))
    .GetAttributes<LogAttribute>()
    .FirstOrDefault();

if (attrib != null)  attrib.BeforeLog();
AddFirstFourNumbers(somenumbers);
if (attrib != null)  attrib.AfterLog();

Now, you could use your lambda approach to make a method that gets this attribute, but you would have to go the expression tree approach (linked below) to get the attribute from inside the lambda call. And now you have an attribute for no real reason. Not fun.

I present this as an alternative. I believe you may be overthinking this.

First, Your Log method, as implemented, won't get the name of the method you're passing into the lambda. You can change how you get the method name by using expressions, but this will mean your lambda calls can't be much more than a single method call, not groups of methods, or property accesses.

I think you are overusing exception handling. I suggest you catch exceptions at the highest level where you can make a decision about how to handle them. There, you'll have the call stack, which will indicate where the exception occurred. You generally do not need to log exceptions where they happened, unless you need to log variables to understand the cause of the exception. (KeyNotFoundException doesn't tell you which key wasn't found!) A general-purpose logging system, as you propose in your question, wouldn't -- couldn't -- grab these variables.

This is more subjective opinion, but I feel that you are overthinking this a bit. This is my recommendation. Keep it simple, and just add some logging to your methods:

public int AddFirstFourNumbers(params int[] numbahs) 
{
    int product;
    product += numbahs[0];
    product += numbahs[1];

    Debug("Still adding numbers but we'll require additional pylons soon");

    product += numbahs[2];
    product += numbahs[3];
    return product;
}

And:

var sum = AddSomeNumbers(somenumbers);
Debug($"{nameof(AddSomeNumbers)} on {somenumbers.Length} numbers -> {sum}");

This allows greater flexibility and utility in your log messages, and doesn't litter your code much more than the lambda approach. This means you'll need to write a log message, but there is no rule they need to all be consistent and fully of juicy debugging tips. Something as simple as Debug($"sum={sum}"); is still useful and doesn't take much time to type.

Okay. This last idea will probably provide you the biggest benefit toward what you want to achieve. Do I have the following stipulations right?

  • You want to know where you are logging from. Class and method. Even the line number.

  • You'd like to know the thread, or the user.

  • You'd like to be able to add additional logging information globally without having to alter your code.

  • You'd like to be able to add additonal logging information to specific classes or namespaces, without having to alter your code.

Use a library like NLog, log4net, Serilog, or others. They're all very good. All of them will let you define a global pattern for your log messages: class name, method name, thread id, user name, timestamp, an environment variable, etc. I am most familiar with NLog, which provides these as layout renderers. I will show you how this works using NLog.

public class YourExampleClass
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public void ExampleMethod()
    {
        try
        {
           Logger.Info("Hello world");
        }
        catch (Exception ex)
        {
           Logger.Error(ex, "Goodbye cruel world");
        }
    }
}  

With that code, the other information I specified, like location and timestamp, are added in configuration using the layout renderers. Neat, huh? This would not require large scale alterations to your program. You can log from the classes you need to log from, and other classes won't be altered.

For your convenience, here is the official NLog tutorial.

  • Thanks for explaining why this will not work. It seems like without access to AOP I won't be able to achieve what I'm asking for because what I'm asking for is specifically covered by AOP with no other workaround for it. At least with the information you've provided I can see why this won't be a valuable use of my time. – Imbaker1234 Jan 09 '20 at 12:41
  • While the pointer to NLog and Log4Net is helpful it's not really the point of this post. There are numerous delegate wrapper methods that I'd like to be able to include via attribute, not just logging. – Imbaker1234 Jan 09 '20 at 12:43
  • @Imbaker1234 I understand. If you *really* want to go with attributes, you could use something like DynamicProxy, but it has a lot of limitations, like only being able to intercept virtual methods. The changes to your application would be more substantial. Or if you have money, PostSharp. –  Jan 09 '20 at 13:16
  • Is it possible to use DynamicProxy in a DLL? Most of the code I write is supplementary DLLs designed to replace or extend legacy applications. – Imbaker1234 Jan 09 '20 at 16:08
  • @Imbaker1234 You'll run into some limitations. You are familiar with the Adapter pattern? It works similarly: you take an instance some object, wrap it with the proxy interceptor, then use the interceptor where you would have used that class. Calling a method on the proxied object will call the interceptor instead. You can wrap the *actual* method call however you like. You can only intercept virtual methods and properties, though. The interceptor would be able to scan for attributes and use them as directed, though, so your attribute usage would be more transparent. –  Jan 09 '20 at 16:30
  • I’ve reread your post numerous times. It’s been very helpful. I finally put together a few Dynamic Proxies using Castle Core. Yet I’m finding I’m able to work with non virtual methods without issue? Maybe I’m doing something wrong here but it doesn’t seem to be experiencing the limitations you spoke of. – Imbaker1234 Jan 15 '20 at 13:37
  • @Imbaker1234 Ask a fresh question, including your code, and I can take a look at it. –  Jan 15 '20 at 14:04
  • Coming back to this message and re-reading it months later its still helpful. Thanks @Amy! – Imbaker1234 Apr 09 '20 at 04:44
  • Is that not polluting a bit too much the source code? What if you need proper bench-marking of those methods? How about using a decorator instead? – progLearner Nov 09 '21 at 09:02