35

Right now (C# 4.0), our logging method looks like

public void Log(string methodName, string messageFormat, params object[] messageParameters)

where the logger does the string formatting, so that the caller does not have to put String.Format's to create a nice log message (and allows for the logger to skip the string formatting if no logviewer is attached).

With C# 5.0, I would like to get rid of the methodName parameter by using the new CallerMemberName attribute but I don't see how this can be combined with the 'params' keyword. Is there a way to do this?

Pang
  • 9,564
  • 146
  • 81
  • 122
Emile
  • 2,200
  • 27
  • 36

3 Answers3

45

You could do something like this:

protected static object[] Args(params object[] args)
{
    return args;
}

protected void Log(string message, object[] args = null, [CallerMemberName] string method = "")
{
    // Log
}

To use the log do like this:

Log("My formatted message a1 = {0}, a2 = {2}", Args(10, "Nice"));
guilhermekmelo
  • 609
  • 1
  • 6
  • 4
20

I believe you simply can't combine params and optional parameters, which are required for CallerMemberName. The best you can do is to use actual array instead of params.

svick
  • 236,525
  • 50
  • 385
  • 514
5

To build on @guilhermekmelo's answer, I might suggest using a chained method:

So keep your current Log(string,string,object[] method:

public void Log(string methodName, string messageFormat, params object[] messageParameters)

And add this new overload (Log(string,string)):

public LogMessageBuilder Log(string messageFormat, [CallerMemberName] string methodName = null)
{
    // Where `this.Log` is 
    return new LogMessageBuilder( format: messageFormat, logAction: this.Log );
}

public struct LogMessageBuilder
{
    private readonly String format;
    private readonly String callerName;
    private readonly Action<String,String,Object[]> logAction;

    public LogMessageBuilder( String format, String callerName, Action<String,String,Object[]> logAction )
    {
        this.format = format;
        this.callerName = callerName;
        this.logAction = logAction;
    }

    public void Values( params Object[] values )
    {
        this.logAction( this.format, this.callerName, values );
    }
}

Used like so:

this.Log( "My formatted message a1 = {0}, a2 = {2}" ).Values( 10, "Nice" );

Note that LogMessageBuilder is a struct, so it's a value-type, which means it won't cause another GC allocation - though the use of params Object[] will cause an array allocation at the call-site. (I wish C# and .NET supported stack-based variadic parameters instead of faking it with a heap-allocated parameter array).


Another option is to use FormattableString - but note that due to how the C# compiler has built-in special-case magic for FormattableString you need to be careful not to let it be implicitly converted to String (also it sucks that you can't add extension-methods to FormattableString directly, grumble):

public void Log(FormattableString fs, [CallerMemberName] string methodName = null)
{
    
    this.Log( messageFormat: fs.Format, methodName: methodName, messageParameters: fs.GetArguments() );
}

Usage:

this.Log( $"My formatted message a1 = {10}, a2 = {"Nice"}" );
Dai
  • 141,631
  • 28
  • 261
  • 374
  • 1
    Thank you for your elaborate reply to this old question. With the introduction of interpolated strings the problem kind of resolved itself. At the call site the code looks much better. – Emile Apr 23 '21 at 08:37