Статьи

Циркулярная ссылка с Mongo Appender может привести к краху вашего процесса

Несколько дней назад я писал в блоге о возможности сохранения журналов log4net в базе данных Mongo , но вы должны знать, что этот метод может быть опасным, если ваши объекты имеют циклические ссылки. Циклическая происходит , когда объект А ссылка на объект B и объект B , прямо или косвенно , ссылка на объект возвращается назад, и это высокий риск , когда вы работаете с Монго Serializer.

Mongo Serializer не любит циклические ссылки (это совершенно приемлемо, поскольку документы с циклическими ссылками не могут быть сохранены в базе данных документов), но проблема заключается в следующем: если вы попытаетесь сериализовать объект, имеющий циклическую ссылку, вы получите исключение StackOverflowException и ваш процесс потерпит крах, как указано в официальной документации MSDN

Начиная с версии .NET Framework 2.0, объект StackOverflowException не может быть перехвачен блоком try-catch, и соответствующий процесс завершается по умолчанию . Следовательно, пользователям рекомендуется писать свой код для обнаружения и предотвращения переполнения стека.

Если вы помните, как я модифицировал приложение MongoDb log4net, я решил сохранить сложные объекты MongoDB с помощью следующего кода:

if (compositeProperties != null && compositeProperties.Count > 0)
{
    var properties = new BsonDocument();
    foreach (DictionaryEntry entry in compositeProperties)
    {
        BsonValue value;
        if (!BsonTypeMapper.TryMapToBsonValue(entry.Value, out value))
        {
            properties[entry.Key.ToString()] = entry.Value.ToBsonDocument();
        }
        else
        {
            properties[entry.Key.ToString()] = value;
        }
    }
    toReturn["customproperties"] = properties;
}

Ключевой момент заключается в entry.Value.ToBsonDocument (), потому что, если кто-то сохранит в глобальном контексте log4Net объект, содержащий циклическую ссылку, ваша программа будет прервана при следующем вызове log4net , поскольку исключение StackOverflowException не может быть перехвачено .

Это особенно раздражает, когда вы хотите сохранить в своем журнале объект, который поступает из базы данных с ORM, например NHibernate , потому что каждый объект, имеющий ссылку на Bag, обычно гидратируется с помощью PersistentBag , внутреннего класса по nhibernate , который имеет циклическую ссылку , Простое решение этого процесса — сообщить MONGO, какой сериализатор использовать для таких специфических типов.

Техника проста, драйверы Mongo предоставляют возможность легко зарегистрировать пользовательский поставщик сериализации.

BsonSerializer.RegisterSerializationProvider(new LoggerBsonSerializerProvider());

И это код класса, который реализует интерфейс ISerializationProvider

public class LoggerBsonSerializerProvider : IBsonSerializationProvider
{
    public IBsonSerializer GetSerializer(Type type)
    {
        if (type.FullName.Contains("Nhibernate", StringComparison.OrdinalIgnoreCase))
        {
               return BsonNullSerializer.Instance;
        }
        return null;
    }
}

Единственная функция, которую вам нужно реализовать — это GetSerializer , в этом простом примере для всех типов, которые содержат строку NHibernate, просто вернуть BsonNullSerializer. Это в основном говорит Mongo Serializer игнорировать эти типы. На мой взгляд, это лучший подход, поскольку он позволяет избежать риска сериализации внутренних классов NHibernate, которые фактически могут вызвать исключение StackOverflowException . Если вы хотите сериализовать NHibernate PersistentGenericBag, но не хотите рисковать циклической ссылкой, вы можете использовать этот код вместо этого.

public class LoggerBsonSerializerProvider : IBsonSerializationProvider
{
    public IBsonSerializer GetSerializer(Type type)
    {
        if (type.FullName.Contains("Nhibernate", StringComparison.OrdinalIgnoreCase))
        {
            if (type.TypeImplementsGenericInterface(typeof(IList<>)))
            {
                    return EnumerableSerializer.Instance;
            }
            return BsonNullSerializer.Instance;
        }
        return null;
    }
}
 
public static Boolean TypeImplementsGenericInterface(this Type typeToCheck, Type interfaceToLookFor)
{
    return typeToCheck.GetInterface(interfaceToLookFor.Name) != null;
}

Основное отличие состоит в том, что для каждого внутреннего типа NHibernate, реализующего общий IList <>, я говорю Mongo сериализовать с использованием EnumerableSerializer, этот тип сериализатора позволяет избежать проблемы циклической ссылки, поскольку PersistentGenericBag обрабатывается как IList <>, игнорируя его действительный свойства. Этот подход по-прежнему небезопасен, поскольку необходимо убедиться, что коллекция уже загружена из базы данных или объект не отсоединен, чтобы избежать исключения во время ведения журнала, поскольку коллекция не может быть инициализирована . Этот тип исключений можно отследить, поэтому он может быть незначительной проблемой, потому что вы можете обработать его с помощью простой попытки try внутри Mongo Appender.