Логирование абстрактных типов используя значения конкретных типов в C#

Есть сервис, читающий сообщения из очереди. Необходимо реализовать логирование некоторых полей приходящих сообщений. Сообщения - разных типов с разным набором полей. Как это выглядит прямо сейчас:

Есть интерфейс IMessage, у которого нет ни свойств, ни методов.

Есть реализации IMessage (названия полей и классов - не настоящие, созданы только для демонстрации примера).

  1. AlertMessage

    public class AlertMessage: IMessage
    {
        public string AlertHeader { get;set; }
        public string AlertId { get;set; }
        // Здесь еще много других полей
    }
    
  2. StatusMessage

    public class StatusMessage: IMessage
    {
        public string StatusId { get;set; }
        public double Speed { get; set; }
        // Здесь еще много других полей
    }
    

Код обработки сообщений:

public async Task ReceiveAndProcessMessagesAsync()
{
    IReadOnlyCollection<IMessage> messages;

    do
    {
        messages = await _receiveService.ReceiveMessagesAsync();

        foreach (var message in messages)
        {
            try
            {
                var messageProcessor = GetMessageProcessor(message);
                await messageProcessor.ProcessMessageAsync();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during processing.");
            }
            finally
            {
                await message.MarkAsProcessedAsync();
                if (_logger.IsEnabled(LogLevel.Debug))
                {
                    _logger.LogDebug("Message processed. {MessageType}. {@Message}", message.GetType().Name, message);
                }
                else
                {
                    _logger.LogInformation("Message processed. {MessageType}", message.GetType());
                }
            }
        }
    } while (messages.Count > 0);
}

private IMessageProcessor GetMessageProcessor(IMessage message)
{
    return message switch
    {
        AlertMessage alertMessage => new AlertMessageProcessor(alertMessage),
        StatusMessage statusMessage => new StatusMessageProcessor(statusMessage),
        //...
        _ => throw new ArgumentOutOfRangeException("Unknown message.")
    };
}

Мне нужно в блоке finally в строке:

_logger.LogInformation("Message processed. {MessageType}", message.GetType());

дополнительно логировать поля сообщений. Т.е. чтобы

  1. для AlertMessage было сообщение

Message processed. {MessageType}, AlertId = {AlertId}, AlertHeader = {AlertHeader}

  1. для StatusMessage было сообщение

Message processed. {MessageType}, StatusId = {StatusId}, Speed = {Speed}

Как можно реализовать такое поведение? Пытаюсь найти каноническое решение в интернете, но не знаю, как правильно загуглить. Можно сделать метод типа LogMessageProcessed(IMessage message), там сделать такой же switch-case, что и в GetMessageProcessor(IMessage message), и в зависимости от типа логировать по-разному. Это первое, что мне приходит в голову. Еще приходит в голову сделать декоратор для процессоров сообщений с логированием.


Ответы (1 шт):

Автор решения: KuzCode

Можно реализовать с помощью рефлексии.

Допустим есть такой класс:

public class Message
{
    public string Text { get; set; }
    public int Id { get; set; }
}

Вот так можно вытянуть свойства:

var message = new Message() { Text = "текст", Id = 0 }; // пример объекта

var propertiesText = message
    .GetType()
    .GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) // выбираете нужные поля
    .Select(property => property.Name + " = " + property.GetValue(message))
    .ToList();

Console.WriteLine(string.Join(", ", propertiesText));

Вывод: Text = текст, Id = 0

Ну и это всё конечно можно свернуть в метод или метод-расширение и потом легко использовать:

public static string GetPropertiesValuesString<T>(this T obj)
{
    var propertiesValues = obj
        .GetType()
        .GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) // выбираете нужные поля
        .Select(property => property.Name + " = " + property.GetValue(obj))
        .ToList();

    return string.Join(", ", propertiesValues);
}
→ Ссылка