是否可以在数据对象中使用Code Contracts的不变量?

时间:2013-11-15 13:39:19

标签: c# poco code-contracts

这个问题直接来自Validating parameters properties with Code Contracts

在该问题中,Ahmed KRAIEMStephen J. Anderson都声明,如果对象的状态正确性的检查必须始终保持在该对象内,则应将其置于该对象内。

但是我遇到了实现它的问题:在将检查实现为Code Contracts的不变量后,我不再能够使用对象初始化,AutoMapper或EntityFramework。

出现问题的原因是它们首先使用默认的空构造函数创建一个新对象,然后填充各种属性,这会触发Code Contracts的不变量,在运行时生成异常。

Visual Studio单元测试中包含的快速示例(您需要通过NuGet添加AutoMapper才能使其正常工作):

namespace Playground.Sandbox
{
    using System.Diagnostics.Contracts;

    using AutoMapper;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class ContractTest
    {
        private const int CatAge = 5;
        private const string CatName = "Tama";
        private const int DogAge = 10;
        private const string DogName = "Poochi";

        static ContractTest()
        {
            Mapper.CreateMap<Dog, Cat>();
        }

        [TestMethod]
        public void EmptyConstructorShouldThrow()
        {
            var cat = new Cat();

            Assert.AreEqual(default(int), cat.Age);
            Assert.AreEqual(default(string), cat.Name);
        }

        [TestMethod]
        public void NonEmptyConstructorShouldNotThrow()
        {
            var cat = new Cat(CatAge, CatName, true);

            Assert.AreEqual(CatAge, cat.Age);
            Assert.AreEqual(CatName, cat.Name);
        }

        [TestMethod]
        public void ObjectInitializerShouldThrow()
        {
            var cat = new Cat { Age = CatAge, Name = CatName };

            Assert.AreEqual(CatAge, cat.Age);
            Assert.AreEqual(CatName, cat.Name);
        }

        [TestMethod]
        public void AutoMapperConversionShouldThrow()
        {
            var dog = new Dog { Age = DogAge, Name = DogName };
            var cat = Mapper.Map<Dog, Cat>(dog);

            Assert.AreEqual(DogAge, cat.Age);
            Assert.AreEqual(DogName, cat.Name);
        }

        private class Cat
        {
            public Cat()
            {
            }

            public Cat(int age, string name, bool doesMeow)
            {
                this.Age = age;
                this.Name = name;
            }

            public int Age { get; set; }

            public string Name { get; set; }

            [ContractInvariantMethod]
            private void ObjectInvariant()
            {
                Contract.Invariant(this.Age > 0);
                Contract.Invariant(!string.IsNullOrWhiteSpace(this.Name));
            }
        }

        private class Dog
        {
            public int Age { get; set; }

            public string Name { get; set; }
        }
    }
}

如果你想知道为什么public Cat(int, string, bool)构造函数没有被使用,那就是愚弄AutoMapper。对于此示例,我需要一个构造函数来初始化整个对象,AutoMapper会识别它并自动使用它来初始化目标对象。但是,我们的数据对象没有这样的构造函数(而且,它们不能,因为它们可以有许多属性)。

唯一通过的测试是NonEmptyConstructorShouldNotThrow,其他测试都是(正确)失败。

出现问题的原因是它们首先使用默认的空构造函数创建一个新对象,然后填充各种属性,这会触发Code Contracts的不变量,在运行时生成异常。

我是否错误地使用了代码合约,或者在使用对象初始化,AutoMapper或EntityFramework时无法在数据对象中实现不变量?

3 个答案:

答案 0 :(得分:1)

Cat的默认构造函数中设置有效值,如下所示:

public Cat()
{
    Age = 1;
    Name = "Cat";
}

一切都会好的。

修改 正如Damien_The_Unbeliever的评论中所述,对象始终应该处于有效状态,因此在使用默认构造函数后它可能不会处于无效状态。

因此,必须AgeName设置有效值!

否则,默认构造函数完全没有任何意义,即使你计划,或者甚至你承诺在之后的某些时候设置你的属性。

唯一的另一种方法是让你的类成为可构建的,意味着你调用一个完成初始化的方法并激活之后使用你的合同检查方法

答案 1 :(得分:0)

在很多情况下,在对象初始化之后首先启动不变检查是很有价值的。

不幸的是,我在这里没有偶然发现一个很好的解决方案,所以它又回过头来选择更好的多个不好的解决方案。我最喜欢的方法是:

class Cat
{
    private bool _initialized;
    public void Initialized()
    {
        _initialized = true;
    }

    [Pure]
    private bool IsInValidState()
    {
        // Disregard Invariant checking until the Initialized() method has been called
        if (!_initialized) return true;

        // Check the state of the class
        if (this.Age <= 0) return false;
        if (string.IsNullOrWhiteSpace(this.Name)) return false;

        return true;
    }

    public int Age { get; set; }

    public string Name { get; set; }

    [ContractInvariantMethod]
    private void ObjectInvariant()
    {
        Contract.Invariant(IsInValidState());
    }
}

您现在可以进行对象初始化,但必须调用Initialized()方法才能启用不变检查:

var cat = new Cat() {Age = 10, Name = "Foo"};
cat.Initialized();

答案 2 :(得分:0)

您的实现违反了ObjectInvariant CodeContract,因为您的合同规定,您的对象在任何时候都必须将这些属性设置为有效值,并且在构造之后,这是不正确的。但是,让调用者在未定义状态下使用您的对象是一个糟糕的API设计。通常,如果对象的默认状态不是有效状态,我将不允许使用默认构造函数。我宁愿使用正确构造对象所需的最小参数集的构造函数,或者使用合同检查的成员的(有效)默认参数初始化对象。