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?

000
  • 26,951
  • 10
  • 71
  • 101
Moriya
  • 7,750
  • 3
  • 35
  • 53
  • Also see http://stackoverflow.com/questions/3948971/c-sharp-4-0-optional-parameters-and-params-do-not-work-together – nawfal May 29 '13 at 05:32

5 Answers5

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);
    }
}
tstone2077
  • 514
  • 4
  • 13
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", ""));
LukeHennerley
  • 6,344
  • 1
  • 32
  • 50
  • @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? – LukeHennerley Jan 24 '13 at 16:17
  • 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. – Moriya Jan 24 '13 at 16:26
  • @Animal I see... What do you think to my edit? Could this maybe help? – LukeHennerley Jan 24 '13 at 16:32
  • 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 – Moriya Jan 24 '13 at 16:38
  • @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. – LukeHennerley Jan 24 '13 at 16:41
  • @Animal such as passing in an array of objects, where the objects could be other classes with overrides of `ToString()` and then you start to customoize your `args` parameter - just as an idea? :) – LukeHennerley Jan 24 '13 at 16:45
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;
Moriya
  • 7,750
  • 3
  • 35
  • 53
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.

nicodemus13
  • 2,258
  • 2
  • 19
  • 31
-2

move all default arguments to right.

A.T.
  • 24,694
  • 8
  • 47
  • 65
  • That generates a compiler error: "Cannot specify a default value for a parameter array" – Moriya Jan 24 '13 at 16:14
  • 1
    You can't do this, atleast put your code into visual studio and see if it compiles first – LukeHennerley Jan 24 '13 at 16:18
  • 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" – Moriya Jan 24 '13 at 16:41