5

I have a logging interface which I extend with some helpful extension methods as to make it possible for me to pass in a format and a list of arguments to avoid having to use string format every time a call the method. (it also help me follow FXCops culture info rules)

So I can call:

logger.Debug("Created {0} with id {1}",typeof(MyObject).Name ,myObject.Id);

Instead of:

logger.Debug(string.Format("Created {0} with id {1}", typeof(MyObject).Name, myObject.Id));

I now found myself in a bit of a tricky situation because it would be immensely helpful to also get some info in the logs about where the logging was written such as the file, method, and line number. This could be achieved with the neat [CallerMemberName], [CallerFilePath], and [CallerLineNumber] attribute.

logger.Debug("Created {0} with id {1}", typeof(MyObject).Name, myObject.Id);

would then give me a log entry such as:

"MyObjectProvider.cs, Provide, line:50 | Created MyObject with id 1564"

The issue here is that the method signature would look like this:

public static void Debug(this ILogger logger, string format [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, params object[] args)

and that is not possible because the [Caller*] attributes makes the parameters optional and that doesn't work with the args parameter.

I also tried to make multiple implementations with fixed amount of strings as parameters like this:

public static void Debug(this ILogger logger, string format [CallerMemberName] string callerMemberName = "",string arg, string arg2 , ...etc... , [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)

but then I get compiler errors saying the "The call is ambiguous between the following methods or properties"

I have almost given up on this issue now but I thought to myself, "Maybe SO can find a solution for me". So here it is... Is it possible to use both params object[] args and [CallerFilePath] in any way or is there another way to get the intended result?

1

5 Answers 5

2

I ran into the same issue, but solved it a little differently. It's not the most elegant solution, but it works and it's relatively clean:

public class SrcLoc
{
    public string sourceFile { get; set; }
    public int lineNumber { get; set; }
    public SrcLoc([CallerFilePath] string sourceFile = "",
                  [CallerLineNumber] int lineNumber = 0)
    {
      this.sourceFile = sourceFile;
      this.lineNumber = lineNumber;
    }
}
public class Logger
{
   public void Log(SrcLoc location,
                int level = 1,
                string formatString = "",
                params object[] parameters)
  {
     string message = String.Format(formatString, parameters);
  }
}
public MainTest
{
    public static void Main()
    {
        string file="filename";
        logger.Log(new SrcLoc(), (int)LogLevel.Debug, "My File: {0}", file);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

You can't combine the two in a method signature. What you could do is one or the other and pass in null to where you need optional parameters, would this work for you?

Foo(s, null);
public void Foo(string s, params string[] sArray)
{

}

Foo(new string[] {""});
private static void Foo(string[] sArray,  string s = "")
{
}

OR

Why not use a class which handles your formatting and make that optional?

public class LogArgs
{
  private string _formatString;
  private string[] _args;
  public LogArgs(string formatString, params string[] args)
  {
    _formatString = formatString;
    _args = args;
  }
  public override string ToString()
  {
    return string.Format(_formatString, _args);
  }
}

public void Foo(string mandatory, LogArgs optionalParam = null)
{
  //Do Stuff
}

Foo("", new LogArgs("{0} is formatted", ""));

6 Comments

@Animal What I am saying is you can keep your methods the same, but you just have to sacrifice either using optional parameters or params. Personally, i'd replace params object[] args with just object[] args and then pass in the array that way - do you understand what I mean?
This would work but the calls to that method would look pretty messy and they would not be very simple to use as you have to create the array of the arguments to the string.format. If that is the sole solution it would probably be better to format the message before logging it.
@Animal I see... What do you think to my edit? Could this maybe help?
Well... is this Foo("", new LogArgs("{0} is formatted", "")); better than Foo("", string.Format("{0} is formatted", ""));? I think it's basically the same and coders know what string.Format so maybe that's a better solution for readability
@Animal I agree, it probably isn't. The only benefit is if you wanted to extend the LogArgs class to do some other stuff that perhaps string.Format couldn't.
|
1

I found another way to get the information I want by using StackTrace. It's a bit unsafe in optimized code and it's very slow but for debuging purposes it works great as long as it's possible to shut it off in release builds.

StackTrace stackTrace = new StackTrace();
var callerMember = stackTrace.GetFrame(1).GetMethod();
var callerMemberName = callerMember.Name;
var callerType = callerMember.ReflectedType.Name;

Comments

0

The most elegant way (or least inelegant way) I've found is to create a method with the required name that extracts the attribute information and returns an Action delegate. You then setup this delegate with the signature that you actually want to call.

So, from

public static void Debug(this ILogger logger, string format, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, params object[] args)

create a delegate

public delegate void LogDelegate(string format, params object[] args);

which is returned from your method call:

public static void Debug(this ILogger logger, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
  return (format, args)
  {
    LogWithCallerSiteInfo(format, args, callerMemberName, callerFilePath, callerLineNumber, logAction);
  }
}

and calls a helper method with the captured data:

private static void LogWithCallerSiteInfo(string format, object[] args, string callerMemberName, string callerFilePath, int callerLineNumber, Action<string, object[]> logRequest)
    {
        if (args == null)
        {
            args = new object[0];
        }
        var args2 = new object[args.Length + 3];
        args.CopyTo(args2, 0);
        args2[args.Length] = sourceFile;
        args2[args.Length + 1] = memberName;
        args2[args.Length + 2] = lineNumber;

        logRequest(format + " [{callerFilePath:l}.{callerMemberName:l}-{callerLineNumber}]", args2);
    }

And make the call, thus:

logger.Debug()("Created {0} with id {1}",typeof(MyObject).Name ,myObject.Id);

So, in usage terms, you insert an extra set of (), which captures the call-site info and the set set makes the call on the delegate. That's as neat as I've managed to make it.

I've recreated the params array adding in the captured data, otherwise, (at least with SeriLog, the results are unpredictable.

Comments

-2

move all default arguments to right.

3 Comments

That generates a compiler error: "Cannot specify a default value for a parameter array"
You can't do this, atleast put your code into visual studio and see if it compiles first
Moving the default arguments to the right makes params be in the middle and that doesn't compile "A parameter array must be the last parameter in a formal parameter list"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.