ORM与CodeContracts结合使用的实体 - 确保不变量

本文关键字:实体 确保 不变量 CodeContracts 结合 ORM | 更新日期: 2023-09-27 18:01:34

我目前正在将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尝试水化这些实体,它将生成"不变失败"异常,因为它尝试设置一个属性一个接一个属性,并且每个属性设置器执行验证不变量的代码。

因此,为了使所有这些工作,我必须确保实体通过其公共构造函数进行实例化。

但是我该怎么做呢?

ORM与CodeContracts结合使用的实体 - 确保不变量

Daniel,如果我没记错的话(自从我与 NH 合作以来已经有一段时间了(,你可以有一个私有构造函数,他仍然应该可以创建你的对象。

除此之外,为什么你需要100%确定?这是某种要求,还是您只是想涵盖所有基础?

我之所以这么问,是因为根据要求,我们可以提供另一种实现它的方法。

您现在可以做的是提供额外的保护,方法是连接一个IInterceptor类,以确保在加载后您的类仍然有效。

我想底线是,如果有人想搞砸你的域和类,无论你做什么,他们都会这样做。在大多数情况下,防止所有这些东西的努力并没有得到回报。

澄清后编辑

如果您使用对象写入数据库并且合约正在工作,则可以安全地假设数据将被正确写入,因此如果没有人篡改数据库,则可以正确加载数据。

如果您确实手动更改了数据库,则应停止执行此操作并使用域执行此操作(这是验证逻辑所在(或测试数据库更改过程。

不过,如果你真的需要它,你仍然可以连接一个IInterceptor,它将在加载后验证你的实体,但我认为你不能通过确保你的房屋管道正常来解决来自街道的水淹

基于与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);
    }
}

DisableInvariantEvaluationEnableInvariantEvaluation 当前是使用反射设置受保护字段的扩展方法。此字段可防止检查不变量。此外,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 需要在 Fluent NHibernate ExposeConfiguration中使用config.AppendListeners(ListenerType.PostLoad, ...);注册为侦听器。
  8. 我们的自定义字节码提供程序需要使用 Environment.BytecodeProvider 进行注册。
  9. 我们的自定义拦截器需要使用config.Interceptor = ...; 注册。

当我有机会从所有这些中创建连贯的包并将其放在 github 上时,我将更新这个答案。
此外,我想摆脱反射并创建一个可以直接访问受保护的 CodeContract 成员的代理类型。

作为参考,以下博客文章有助于实现多个 NHibernate 接口:

  • http://weblogs.asp.net/ricardoperes/archive/2012/06/19/implementing-an-interceptor-using-nhibernate-s-built-in-dynamic-proxy-generator.aspx
  • http://kozmic.net/2011/03/20/working-with-nhibernate-without-default-constructors/

遗憾的是,对于具有复合键的实体,这目前会失败,因为反射优化器不用于它们。这实际上是NHibernate中的一个错误,我在这里报告了它。