How to use params keyword along with caller Information in C#?

只愿长相守 提交于 2019-12-05 02:22:32

I don't think that it can be done in exactly the way you want to do it. However, I could think of a few viable workarounds which would probably give you almost the same benefits.

  1. Use an intermediate method call to capture the caller member name. The first method call returns a delegate which can in turn be called to provide the additional parameters. This looks weird, but it should work:

    log.Log()("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
    "Scotty", warpFactor);
    

    One disadvantage here is that it's possible to call log.Log("something"), expecting that your message will be logged, and nothing will happen. If you use Resharper, you can mitigate this by adding a [Pure] attribute to the Log() method so you get a warning if someone doesn't do anything with the resulting object. You could also tweak this approach slightly, saying:

    var log = logFactory.GetLog(); // <--injects method name.
    log("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
        "Scotty", warpFactor);
    
  2. Produce your log messages with lambdas, and let string.Format take care of the params array:

    log.Log(() => string.Format("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
        "Scotty", warpFactor));
    

    This is the approach that I typically use, and it has some side advantages:

    1. Your log method can catch exceptions produced while producing the debug string, so instead of breaking your system you just get an error that says: "Failed to produce log message: [exception details]".
    2. Sometimes the object you pass to your format string might incur additional cost, which you'd only want to incur when you need it:

      log.Info(() => string.Format("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
          _db.GetCurrentUsername(), warpFactor));
      

      You'd prefer not to have the above code do a database trip if info-level logging is not turned on.

    As a side note, I find myself using string.Format often enough that I've created a helper method to shorten the syntax slightly:

    log.Log(() => "{0}: I canna do it cap'n, the engines can't handle warp {1}!" 
        .With("Scotty", warpFactor));
    

To go along with StriplingWarrior suggestion instead of a delegate you could do it with a fluent syntax.

public static class Logger
{
    public static LogFluent Log([CallerMemberName] string sourceMemberName = null)
    {
        return new LogFluent(sourceMemberName);
    }
}

public class LogFluent
{
    private string _callerMemeberName;

    public LogFluent(string callerMamberName)
    {
        _callerMemeberName = callerMamberName;
    }

    public void Message(string message, params object[] messageArgs)
    {

    }
}

Then call it like

Logger.Log().Message("{0}: I canna do it cap'n, the engines can't handle warp {1}!", "Scotty", 10);

Logger doesn't have to be static but it was a simple way to demonstrate the concept

Well, let me mention one option; you can use reflection to get exact same name, as CallMemberName. This will be definitely slower. Assuming you don't log every millisecond, it will be enough to handle the pressure, I believe.

var stackTrace = new StackTrace(); var methodName = stackTrace.GetFrame(1).GetMethod().Name;

I like following Jim Christophers (@beefarino) pluralsight course to setup my own logging projects. For that I clone the ILog interface as ILogger, I implement that - lets say in a class called LoggerAdapter - and then I use Jim Christophers LogManager to have a GetLogger(Type type)-Method that returns a wrapped log4net-logger 'LoggerAdapter':

namespace CommonLogging
{
    public class LogManager : ILogManager
    {
        private static readonly ILogManager _logManager;

        static LogManager()
        {
            log4net.Config.XmlConfigurator.Configure(new FileInfo("log4net.config"));
            _logManager = new LogManager();
        }

        public static ILogger GetLogger<T>()
        {
            return _logManager.GetLogger(typeof(T));
        }

        public ILogger GetLogger(Type type)
        {
            var logger = log4net.LogManager.GetLogger(type);
            return new LoggerAdapter(logger);
        }
    }
}

The next step is to create a generic extension like this, setting the caller-information into a ThreadContext.Property:

public static class GenericLoggingExtensions
{
    public static ILogger Log<TClass>(this TClass klass, [CallerFilePath] string file = "", [CallerMemberName] string member = "", [CallerLineNumber] int line = 0)
        where TClass : class
    {
        ThreadContext.Properties["caller"] = $"[{file}:{line}({member})]";
        return LogManager.GetLogger<TClass>();
    }
}

Having that in place I can always call up a logger before writing an actual method by just calling:

this.Log().ErrorFormat("message {0} {1} {2} {3} {4}", "a", "b", "c", "d", "e");

and if you have the conversionPattern of your PatternLayout configured to use the property:

<layout type="log4net.Layout.PatternLayout">
  <conversionPattern value="%utcdate [%thread] %-5level %logger - %message - %property{caller}%newline%exception" />
</layout>

you will always have a proper output with callerinformation of the first .Log()-call:

2017-03-01 23:52:06,388 [7] ERROR XPerimentsTest.CommonLoggingTests.CommonLoggingTests - message a b c d e - [C:\git\mine\experiments\XPerimentsTest\CommonLoggingTests\CommonLoggingTests.cs:71(Test_Debug_Overrides)]
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!