-1

I have a class instance (Eli) which is used in multiple contexts, and which needs to log messages, independent of (but correctly in each) context:

public class Eli
{
    void LogMessage(string msg)
    {
        ///what to do here?
    }

    public void GrillTheCat()
    {
        LogMessage("I deed it";)
    }
}


public class EliWrapper
{
    Eli _eli;
    Action<string> _logAction;

    public EliWrapper(Eli eli, Action<string> logAction)
    {
        _eli = eli;
        _logAction = logAction;
    }

    public void GrillTheCat()
    {
        _eli.GrillTheCat(); //I want LogMessage in Eli to invoke the _logAction of this calling instance
    }
}

var eli = new Eli();

var wrapper1 = new EliWrapper(eli, msg => Console.WriteLine(msg));
var wrapper2 = new EliWrapper(eli, msg => File.AppendAllText(msg + "\n"));

I realize I could pass in the logger to the GrillTheCat function, but in my real situation, Eli has >10 functions and I don't want to clutter up all of the function signatures just for the sake of logging.

I also realize I could define a LogAction property on Eli, then have the wrappers assign their _logAction value to that property prior to invoking Eli's function, but again I have many functions and it would be somewhat tedious to wrap each one.

What I'm hoping for is a reflection-based solution where Eli's LogMessage function just steps up a couple layers of the call stack, and accesses the wrapper instance's _logAction directly.

odkken
  • 329
  • 1
  • 9

3 Answers3

2

What I'm hoping for is a reflection-based solution where Eli's LogMessage function just steps up a couple layers of the call stack, and accesses the wrapper instance's _logAction directly.

I wasn't able to find any reasonable way to access instances outside the current executing method without you heavily modifying signatures(you stated you didn't want to do).

Although I generally would not recommend what you're trying to do because of the tight coupling and general lack of extensibility and intuitiveness - However, I figured out a solution that almost fits the bill.

It is not possible, at least from what I was able to research, to access instance data from calling members. Which is to say you can't walk back up the stack and access instanced variables or objects all will-nilly, unless you explicitly capture and pass them down the stack as you're - err.. um "stacking"?.

The way we work around this is simply by declaring your _logAction as a static member. That way we don't need to access the instance you have of EliWrapper.

What this doesn't do for you is allow you to have multiple EliWrappers with different _logAction's becuase they're static.

Unfortunately without access to the individual instance(which you can't get from the stack - there's no way for Eli to know what EliWrapper wants to do without at least some of the modifications you explicitly wanted to avoid(In my opinion).

Where do we go from here?

Consider

  • Consider Modifying Eli so it can be used as a base-class that has different versions that log things differently.
  • Consider Modifying Eli to implement overrides that accept a Action<string> as a override for it's default logging.

Alternatively, but not recommended

  • Pass the instance of the caller to Eli so it can access instanced(non-static) members on EliWrapper so you don't need to make _logAction static(this would be a simple modification to the code i have provided to you, but would require changing all of Eli's signatures to accommodate object caller.
  • Store instances of EliWrapper somewhere you can access without instance, such as a static class, where you can access their instance data using reflection without explicitly passing their instances to Eli

Here's the script to access the static field using the stack

public class Eli
{
    private readonly Action<string> DefaultLogger = (s) => Console.WriteLine(s);

    void LogMessage(string msg)
    {
        // get the stack so we can get advanced information about
        // who called us (CallerMemberNameAttribute was another alternative, but would incur more complex code)
        StackTrace stack = new(false);

        // step 2 frames up(or however many to get out of Eli and back to the 'caller'
        var caller = stack.GetFrame(2)?.GetMethod()?.DeclaringType;

        if (caller != null)
        {
            // check to see if the type that called GrillTheCat()
            // has a static private field with the name '_logAction'
            var possibleLoggerInCaller = caller.GetField("_logAction", BindingFlags.Static | BindingFlags.NonPublic);

            if (possibleLoggerInCaller != null)
            {
                // get the static value of that field
                var possibleLogger = possibleLoggerInCaller.GetValue(null);

                // verify that the type of that logger is infact a Action<string>
                // since that's what we use to log
                if (possibleLogger is Action<string> logger)
                {
                    // log the msg using the overriden logger instead of the default one
                    logger.Invoke(msg);

                    return;
                }
            }
        }


        // if we got here there wasn't a _logAction in the call stack at frame 2
        // so give up and use our default logger
        DefaultLogger.Invoke(msg);
    }

    public void GrillTheCat()
    {
        LogMessage("I deed it");
    }
}


public class EliWrapper
{
    Eli _eli;
    private static Action<string> _logAction;

    public EliWrapper(Eli eli, Action<string> logAction)
    {
        _eli = eli;
        _logAction = logAction;
    }

    public void GrillTheCat()
    {
        _eli.GrillTheCat(); //I want LogMessage in Eli to invoke the _logAction of this calling instance
    }
}
DekuDesu
  • 2,224
  • 1
  • 5
  • 19
  • Thanks for the detailed reply. Unfortunately, the main requirement is that I am able to have multiple wrappers, so the static approach won't work. I think the most straightforward way is to have Eli throw exceptions instead of logging, since the intent of this is for only the calling wrapper to be aware of the message, said messages are only in the event of an error, and an exception will directly bubble up the stack the way I want it to. – odkken Jun 02 '21 at 01:41
  • 1
    *said messages are only in the event of an error* this would have been critical information to include in your question to better assist answerers in providing you more accurate and helpful answers. – DekuDesu Jun 02 '21 at 09:09
0

For my needs, I've gone with throwing exceptions. This procedurally does what I asked: only notifies the calling instance of the message, and requires no modification of function signatures.

odkken
  • 329
  • 1
  • 9
0

Consider implementing a decorator for Eli that implements logging. Here is a rudimentary example that demonstrates this:

// If you haven't already: define an interface for Eli
public interface IEli
{
   // Define all Eli's public members
}

// Let Eli implement IEli
public class Eli : IEli
{
    ...
}

With the existence of the new IEli interface, you can now implement a decorator:

public class LoggingEli : IEli
{
    private readonly IEli decoratee;
    private readonly Action<string> logAction;

    public LoggingEli(IEli decoratee, Action<string> logAction)
    {
        this.decoratee = decoratee;
        this.logAction = logAction;
    }

    // Implement all IEli members by calling the log action and forwarding
    // the call to the decorated IEli instance:
    public object SomeEliMethod(string param1, int param2)
    {
        this.logAction(nameof(SomeEliMethod) + " called for " + param1);
        return this.decoratee.SomeEliMethod(param1, param2);
    }

    // Same for all other 9 IEli methods.
}

Using the new IEli interface and the LoggingEli decorator, you can now construct the following object graph:

var eli = new Eli();

var consoleEli = new LoggingEli(eli, msg => Console.WriteLine(msg));
var fileEli = new LoggingEli(eli, msg => File.AppendAllText(msg + "\n"));

Decorators have the advantage that you are able to add behavior to a class without having to change the original class. Downside is that it is only possible to add behavior at the start or end of the original method, and the behavior only has access to all the parameters going in and out of the called method. In your case, you can't log halfway the method, and can't log anything information that is kept internal to Eli.

In case you need to log halfway or use information that is internal to Eli, you will need to inject the logger into Eli's constructor.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • Yeah the issue with this approach is that I do in fact need to log from within Eli (e.g. halfway through the method), in the case of an error. Eli contains business logic, and the details of violations of that logic should be spit out where it happens. And again, I can't inject the logger into Eli since there can only be one instance of Eli, but I have multiple logging implementations. I think exceptions really are the right approach here since the nature of these messages are only business logic violations. – odkken Jun 03 '21 at 18:03
  • When it comes to logging, you might be interested in reading this: https://stackoverflow.com/a/9915056. – Steven Jun 03 '21 at 18:41