Сущности, используемые ORM в сочетании с CodeContracts — обеспечение инвариантов

В настоящее время я добавляю CodeContracts в свою существующую кодовую базу.
Одна вещь, которая оказывается трудной, — это использование сущностей, которые гидратированы NHibernate.

Предположим, что этот простой класс:

public class Post
{
    private Blog _blog;

    [Obsolete("Required by NHibernate")]
    protected Post() { }

    public Post(Blog blog)
    {
        Contract.Requires(blog != null);
        _blog = blog;
    }

    public Blog Blog
    {
        get
        {
            Contract.Ensures(Contract.Result<Blog>() != null);
            return _blog;
        }
        set
        {
            Contract.Requires(value != null);
            _blog = value;
        }
    }

    [ContractInvariantMethod]
    private void Invariants()
    {
        Contract.Invariant(_blog != null);
    }
}

Этот класс пытается защитить инвариант _blog != null. Однако в настоящее время он терпит неудачу, потому что я легко могу создать экземпляр Post, наследуя его и используя защищенный конструктор. В этом случае _blog будет null.
Я пытаюсь изменить свою кодовую базу таким образом, чтобы инварианты действительно были защищены.

Защищенный конструктор на первый взгляд нужен NHibernate для создания новых экземпляров, но есть способ обойти это требование.
Этот подход в основном использует FormatterServices.GetUninitializedObject. Важным моментом является то, что этот метод не запускает никаких конструкторов.
Я мог бы использовать этот подход, и это позволило бы мне избавиться от защищенного конструктора. Статическая проверка CodeContracts теперь будет счастлива и больше не будет сообщать о нарушениях, но как только NHibernate попытается гидратировать такие сущности, она сгенерирует «инвариантные неудачные» исключения, потому что она пытается установить одно свойство за другим, и каждый установщик свойств выполняется. код, который проверяет инварианты.

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

Но как мне это сделать?


person Daniel Hilgarth    schedule 27.02.2013    source источник


Ответы (2)


Даниэль, если я не ошибаюсь (давно я не работал с NH), у вас может быть частный конструктор, и он все равно будет хорошо создавать ваш объект.

Кроме того, зачем вам быть уверенным на 100%? Это какое-то требование или вы просто пытаетесь охватить все основы?

Я спрашиваю об этом, потому что в зависимости от требования мы можем найти другой способ его достижения.

Что вы МОЖЕТЕ сделать прямо сейчас, чтобы обеспечить дополнительную защиту, так это подключить класс IInterceptor, чтобы убедиться, что после загрузки ваш класс все еще действителен.

Я предполагаю, что суть в том, что если кто-то хочет испортить ваш домен и классы, он СДЕЛАЕТ это независимо от того, что вы делаете. Усилия по предотвращению всего этого в большинстве случаев не окупаются.

Изменить после уточнения

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

Если вы меняете базу данных вручную, вы должны либо прекратить это делать и использовать для этого свой домен (вот где логика проверки), либо протестировать процесс изменения базы данных.

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

person tucaz    schedule 27.02.2013
comment
Спасибо за Ваш ответ. Насколько мне известно, частный конструктор никогда не был вариантом - по крайней мере, если вы не хотите создавать эти классы-перехватчики. Цель не в том, чтобы защищаться от злоумышленников. Цель состоит в том, чтобы быстро выйти из строя в случае ошибки в коде или данных. Если данные в базе данных не соответствуют контракту - может быть, потому, что последнее обновление базы данных не прошло должным образом? - Я хочу получить исключение, как только NHibernate попытается гидратировать такой объект. - person Daniel Hilgarth; 27.02.2013
comment
Спасибо. Но я не согласен с обновлением. Здесь есть две системы: база данных и приложение. Оба защищают свои границы. База данных через FK, PK, уникальные индексы и т. д. И приложение через кодовые контракты. Дело не в аттестации, на самом деле. Речь идет о наличии действительного объекта. Одной из целей Code Contracts является быстрый отказ. И это то, что я хочу сделать. Как я уже говорил, никому не нужно ничего вмешиваться, чтобы договор разорвался. Небольшая ошибка в отображении — это все, что нужно. - person Daniel Hilgarth; 27.02.2013
comment
И у меня скорее есть исключение нарушения контракта, требующее InvoiceNumber != null, когда сущность создается, чем случайное NullReferenceException где-то в моем коде. - person Daniel Hilgarth; 27.02.2013
comment
То, чего вы пытаетесь достичь, очень сложно, и лично я никогда раньше не видел для этого хорошего решения. Я был там, где вы находитесь, и я, когда ответ стал настолько сложным, насколько это становится, я понял, что это того не стоило. ORM (по крайней мере, NH) не совместимы с такой проверкой из коробки. В итоге я получил метод IsValid и вызвал его перед сохранением и после загрузки объекта. Вы можете добавить его как часть интерфейса IEntityValidator и перехватить NH после того, как он загрузит вызов метода IsValid. - person tucaz; 27.02.2013
comment
Другой вариант, который я придумал, но с нетривиальной реализацией, заключался бы в том, чтобы иметь класс Builder для создания ваших объектов и каким-то образом сообщать NHibernate, чтобы он регидратировал их, используя его. В конце убедиться, что один экземпляр действителен, можно только тогда, когда вы уверены, что будет вызвана точка проверки, а этого не происходит, когда у вас есть такие варианты. - person tucaz; 27.02.2013
comment
Спасибо за ваши предложения. Я понимаю, что это тяжело. Тем более, что я не хочу добавлять поддержку этого в свои сущности. Но я думаю, что на самом деле это должно быть возможно, потому что все классы, содержащие контракты и инварианты, уже содержат метод, обеспечивающий выполнение инвариантов. Возможно, вызов этого метода является решением. Это будет реализация варианта 3 из моего вопроса. - person Daniel Hilgarth; 27.02.2013
comment
О Builder: Да, я тоже думал об этом, но это потребовало бы слишком много ручного труда. Для каждой сущности мне пришлось бы написать соответствующий билдер. - person Daniel Hilgarth; 27.02.2013
comment
Если такой метод доступен, используйте перехватчик, чтобы вызвать его после того, как NHibernate загрузит вашу сущность. Это должно делать свое дело. - person tucaz; 27.02.2013
comment
Проблема в том, что он легко недоступен, потому что он автоматически генерируется переписчиком сборки CodeContracts и защищается. Мне придется использовать отражение, чтобы иметь возможность вызывать его. Но да, я думаю, это путь. - person Daniel Hilgarth; 27.02.2013
comment
Сделайте это и выпустите как плагин NHibernate/CC :) - person tucaz; 27.02.2013
comment
Хм, это даже сложнее, чем я думал. Инварианты проверяются в конце каждого члена. Это делает невозможным использование моих сущностей такими, какие они есть, потому что первое свойство, которое NHibernate пытается установить, приведет к инвариантному ошибочному исключению, потому что инварианты для полей других свойств ложны, потому что они еще не были установлены . Это означает, что остался только вариант №1 из моего вопроса. Необходимо использовать публичный конструктор. Думаю, чтобы иметь возможность сделать это, мы вернулись к застройщику. - person Daniel Hilgarth; 27.02.2013
comment
Кроме того, ленивые свойства также являются проблемой. Как только контракт станет более продвинутым, чем проверка not null, он оценит ленивые свойства и извлечет данные. - person Daniel Hilgarth; 27.02.2013
comment
В этом есть смысл. Я предполагаю, что вы идете к сложности, когда заставить ее работать не стоит. - person tucaz; 27.02.2013
comment
Я думаю, это было бы возможно: я могу сказать движку CodeContracts во время выполнения, что определенные нарушения следует игнорировать, то есть пока NH заполняет свойства. Но это кажется довольно хакерским, потому что я не нашел реальной точки входа в NH после завершения загрузки, чтобы сказать Code Contracts прекратить игнорировать нарушения, и я не могу игнорировать нарушения Contract для определенного экземпляра. Только для определенного типа, что делает его довольно уязвимым в многопоточных средах. Но я еще не совсем сдался... ;) - person Daniel Hilgarth; 27.02.2013
comment
Кажется, я нашел возможное решение. Пожалуйста, посмотрите мой ответ, если вам интересно. - person Daniel Hilgarth; 28.02.2013

На основе обсуждения с tucaz я пришел к следующему, по своей сути достаточно простому решению:

Сердцем этого решения является класс NHibernateActivator. У него две важные цели:

  1. Создайте экземпляр объекта, не вызывая его конструкторы. Для этого используется FormatterServices.GetUninitializedObject.
  2. Предотвратите запуск исключений «инвариантный сбой», пока NHibernate гидратирует экземпляр. Это двухэтапная задача: отключите проверку инвариантов до того, как NHibernate начнет гидратацию, и снова включите проверку инвариантов после завершения работы NHibernate.
    Первую часть можно выполнить сразу после создания экземпляра.
    Вторая часть используя интерфейс IPostLoadEventListener.

Сам класс довольно прост:

public class NHibernateActivator : INHibernateActivator, IPostLoadEventListener
{
    public bool CanInstantiate(Type type)
    {
        return !type.IsAbstract && !type.IsInterface &&
               !type.IsGenericTypeDefinition && !type.IsSealed;
    }

    public object Instantiate(Type type)
    {
        var instance = FormatterServices.GetUninitializedObject(type);
        instance.DisableInvariantEvaluation();
        return instance;
    }

    public void OnPostLoad(PostLoadEvent @event)
    {
        if (@event != null && @event.Entity != null)
            @event.Entity.EnableInvariantEvaluation(true);
    }
}

DisableInvariantEvaluation и EnableInvariantEvaluation в настоящее время являются методами расширения, которые используют отражение для установки защищенного поля. Это поле предотвращает проверку инвариантов. Кроме того, EnableInvariantEvaluation выполнит метод, проверяющий инварианты, если он будет передан true:

public static class CodeContractsExtensions
{
    public static void DisableInvariantEvaluation(this object entity)
    {
        var evaluatingInvariantField = entity.GetType()
                                             .GetField(
                                                 "$evaluatingInvariant$", 
                                                 BindingFlags.NonPublic | 
                                                 BindingFlags.Instance);
        if (evaluatingInvariantField == null)
            return;
        evaluatingInvariantField.SetValue(entity, true);
    }

    public static void EnableInvariantEvaluation(this object entity,
                                                 bool evaluateNow)
    {
        var evaluatingInvariantField = entity.GetType()
                                             .GetField(
                                                 "$evaluatingInvariant$", 
                                                 BindingFlags.NonPublic | 
                                                 BindingFlags.Instance);
        if (evaluatingInvariantField == null)
            return;
        evaluatingInvariantField.SetValue(entity, false);

        if (!evaluateNow)
            return;
        var invariantMethod = entity.GetType()
                                    .GetMethod("$InvariantMethod$",
                                               BindingFlags.NonPublic | 
                                               BindingFlags.Instance);
        if (invariantMethod == null)
            return;
        invariantMethod.Invoke(entity, new object[0]);
    }
}

Остальное — сантехника NHibernate:

  1. Нам нужно реализовать перехватчик, использующий наш активатор.
  2. Нам нужно реализовать оптимизатор отражения, который возвращает нашу реализацию IInstantiationOptimizer. Эта реализация в свою очередь снова использует наш активатор.
  3. Нам нужно реализовать фабрику прокси, которая использует наш активатор.
  4. Нам нужно реализовать IProxyFactoryFactory, чтобы вернуть нашу собственную фабрику прокси.
  5. Нам нужно создать собственный прокси-валидатор, которому все равно, имеет ли тип конструктор по умолчанию.
  6. Нам нужно реализовать провайдер байт-кода, который возвращает наш оптимизатор отражения и фабрику прокси-фабрики.
  7. NHibernateActivator необходимо зарегистрировать в качестве слушателя, используя config.AppendListeners(ListenerType.PostLoad, ...); в ExposeConfiguration Fluent NHibernate.
  8. Наш собственный поставщик байт-кода должен быть зарегистрирован с использованием Environment.BytecodeProvider.
  9. Наш собственный перехватчик должен быть зарегистрирован с использованием config.Interceptor = ...;.

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

Для справки, следующие сообщения в блоге были полезны при реализации нескольких интерфейсов NHibernate:


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

person Daniel Hilgarth    schedule 28.02.2013
comment
Хотя он использует много сложных точек расширения и скрытых методов, это простое решение. Потрясающий! - person tucaz; 28.02.2013