Как использовать ключевое слово params вместе с информацией о вызывающем абоненте в C#?

Я пытаюсь объединить информацию о вызывающей стороне C# 5.0 вместе с ключевым словом C# params. Намерение состоит в том, чтобы создать оболочку для каркаса журналирования, и мы хотим, чтобы регистратор форматировал текст как String.Format. В предыдущих версиях метод выглядел так:

void Log(
   string message,
   params object[] messageArgs = null);

И мы называем это так:

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

Теперь мы хотим получить информацию о вызывающем абоненте и зарегистрировать ее. Таким образом, подпись становится:

void Log(
    string message,
    params object[] messageArgs,
    [CallerMemberName] string sourceMemberName = null);

Это не компилируется, потому что параметры должны быть последним параметром. Итак, я пытаюсь это:

void Log(
    string message,
    [CallerMemberName] string sourceMemberName = null,
    params object[] messageArgs);

Есть ли способ вызвать это без предоставления sourceMembername или явного присвоения аргумента messageArgs в качестве именованного параметра? Это противоречит цели ключевого слова params:

// params is defeated:
log.Log("message", 
        messageArgs: new object[] { "Scotty", warpFactor });
// CallerMemberName is defeated:
log.Log("message", null,
        "Scotty", warpFactor);

Есть ли способ сделать это? Кажется, что "хакерский" способ передачи информации о вызывающем абоненте исключает использование ключевого слова params. Было бы замечательно, если бы компилятор C# распознал, что параметры информации о члене вызывающей стороны вообще не являются параметрами. Я не вижу необходимости когда-либо передавать их явно.

Моя резервная копия будет состоять в том, чтобы пропустить ключевое слово params, и вызывающая сторона должна будет использовать длинную подпись в этом последнем примере.

4 ответа

Решение

Я не думаю, что это можно сделать именно так, как вы хотите. Однако я мог бы подумать о нескольких жизнеспособных обходных решениях, которые, вероятно, дали бы вам почти такие же преимущества.

  1. Используйте промежуточный вызов метода для захвата имени члена вызывающей стороны. Первый вызов метода возвращает делегата, который, в свою очередь, может быть вызван для предоставления дополнительных параметров. Это выглядит странно, но должно работать:

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

    Одним из недостатков здесь является то, что можно позвонить log.Log("something"), ожидая, что ваше сообщение будет зарегистрировано, и ничего не произойдет. Если вы используете Resharper, вы можете смягчить это, добавив [Pure] приписать Log() метод, чтобы вы получили предупреждение, если кто-то ничего не делает с результирующим объектом. Вы также можете слегка изменить этот подход, сказав:

    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. Создайте свои сообщения журнала с лямбдами и позвольте string.Format позаботиться о массиве params:

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

    Это подход, который я обычно использую, и он имеет некоторые побочные преимущества:

    1. Ваш метод журнала может перехватывать исключения, возникающие при создании строки отладки, поэтому вместо взлома системы вы просто получите сообщение об ошибке: "Не удалось создать сообщение журнала: [подробности об исключении]".
    2. Иногда объект, который вы передаете в строку формата, может повлечь дополнительные расходы, которые вы захотите понести только тогда, когда вам это нужно:

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

      Вы бы предпочли, чтобы вышеприведенный код не выполнял отключение базы данных, если ведение журнала на информационном уровне не включено.

    В качестве примечания я использую string.Format достаточно часто, чтобы создать вспомогательный метод для небольшого сокращения синтаксиса:

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

Чтобы согласиться с предложением StriplingWarrior вместо делегата, вы можете сделать это с помощью свободного синтаксиса.

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)
    {

    }
}

Тогда назови это как

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

Регистратор не должен быть статичным, но это был простой способ продемонстрировать концепцию

Хорошо, позвольте мне упомянуть один вариант; Вы можете использовать отражение, чтобы получить точно такое же имя, как CallMemberName, Это будет определенно медленнее. Полагаю, что вы не регистрируете каждую миллисекунду, этого будет достаточно, чтобы справиться с давлением.

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

Я предпочитаю проходить курс множественного взгляда Джима Кристофера (@beefarino), чтобы настроить свои собственные проекты ведения журналов. Для этого я клонирую интерфейс ILog как ILogger, я реализую это, скажем, в классе с именем LoggerAdapter, а затем я использую Jim Christophers LogManager, чтобы получить GetLogger(Type type)-Method, который возвращает упакованный 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);
        }
    }
}

Следующим шагом является создание общего расширения, подобного этому, установка информации о вызывающем абоненте в 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>();
    }
}

Имея это на месте, я всегда могу вызвать регистратор перед написанием реального метода, просто вызвав:

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

и если у вас есть ConversionPattern вашего PatternLayout, настроенный для использования свойства:

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

у вас всегда будет правильный вывод с информацией о вызове первого.Log()- вызов:

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)]
Другие вопросы по тегам