Обекти, използвани от 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 се опита да хидратира такива обекти, той ще генерира изключения „invariant failed“, защото се опитва да зададе едно свойство след друго и всеки инструмент за настройка на свойства се изпълнява код, който проверява инвариантите.

Така че, за да направя всичко това да работи, ще трябва да се уверя, че обектите са създадени чрез техния публичен конструктор.

Но как да направя това?


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


Отговори (2)


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

Освен това, защо трябва да сте 100% сигурни? Изискване ли е по някакъв начин или просто се опитвате да покриете всички основи?

Питам това, защото в зависимост от изискването бихме могли да предложим друг начин за постигането му.

Това, което МОЖЕТЕ да направите точно сега, за да осигурите допълнителна защита, е да свържете клас IIinterceptor, за да сте сигурни, че след натоварването вашият клас е все още валиден.

Предполагам, че най-важното е, че ако някой иска да обърка вашия домейн и класове, той ЩЕ го направи, независимо какво правите. Усилията да се предотвратят всички тези неща в повечето случаи не се отплащат.

Редактиране след изясняване

Ако използвате вашите обекти, за да пишете в базата данни и вашите договори работят, можете спокойно да предположите, че данните ще бъдат записани правилно и следователно ще бъдат заредени правилно, ако никой не се намеси в базата данни.

Ако промените базата данни ръчно, трябва или да спрете да го правите и да използвате вашия домейн, за да направите това (това е логиката за валидиране), или да тествате процеса на промяна на базата данни.

И все пак, ако наистина имате нужда от това, все още можете да свържете IIinterceptor, който ще потвърди вашия обект след натоварването, но не мисля, че коригирате наводняване от улицата, като се уверите, че тръбата на къщата ви е наред.

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
Относно конструктора: Да, мислех и за това, но ще изисква твърде много ръчни усилия. За всеки обект ще трябва да напиша съответния строител. - 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
Освен това мързеливите свойства също са проблем. Веднага щом договорът е по-напреднал от не null проверка, той ще оцени мързеливите свойства и ще извлече данните. - person Daniel Hilgarth; 27.02.2013
comment
Това има смисъл. Предполагам, че вървите към сложността, при която да го накарате да работи не си струва. - person tucaz; 27.02.2013
comment
Предполагам, че би било възможно: мога да кажа на двигателя на CodeContracts по време на изпълнение, че определени нарушения трябва да се игнорират, т.е. докато NH попълва свойствата. Но ми се струва доста хакерско, защото не намерих истинска входна точка в NH след приключване на зареждането, за да кажа на договорите за кодове да спрат да игнорират нарушенията и не мога да игнорирам нарушенията на договора за определен случай. Само за определен тип, което го прави доста уязвим в многонишкови среди. Но още не съм се отказала напълно... ;) - person Daniel Hilgarth; 27.02.2013
comment
Мисля, че намерих възможно решение. Моля, вижте отговора ми, ако се интересувате. - person Daniel Hilgarth; 28.02.2013

Въз основа на дискусията с tucaz, стигнах до следното, в основата си доста просто решение:

Сърцето на това решение е класът NHibernateActivator. Има две важни цели:

  1. Създайте екземпляр на обект, без да извиквате неговите конструктори. Той използва FormatterServices.GetUninitializedObject за това.
  2. Предотвратете задействането на изключения „invariant failed“, докато 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