Wednesday, July 22, 2020

PostSharp Contracts

One of the things PostSharp does well is remove boilerplate code. That's the repetitive stuff we hate typing and which clutters up and obscures the real code.

Contracts replace parameter and property validation. Let's consider a class that represents a box. It has length, height, and width properties that must be greater than zero - otherwise the box doesn't exist!

The PostSharp Contracts documentation is good, but I couldn't find a list of all the contracts anywhere so here's the ones I could find.

  • CreditCard
  • EmailAddress
  • EnumDataType
  • GreaterThan
  • LessThan
  • Negative
  • NotEmpty
  • NotNull
  • Phone
  • Positive
  • Range
  • RegularExpression
  • Required
  • StrictlyGreaterThan
  • StrictlyLessThan
  • StrictlyNegative
  • StrictlyPositive
  • StrictRange
  • StringLength
And, of course, you can create custom contracts.


Start a new Visual Studio project called Box. I'm using 2019, C#, and .Net Framework. Rename Class1 to Box. The initial code for Box is...

namespace Box
{
    public class Box
    {
        public int length { get; set; }
        public int width { get; set; }
        public int height { get; set; }

        public Box() { }
        public static Box CreateBoxFromLengthWidthHeight(int length, int width, int height)
        {
            return new Box() { length = length, height = height, width = width };
        }
    }
}

We would normally write validation logic into the property setters but this gets messy.

        private int _length;
        public int length
        {
            get { return _length; }
            set
            {
                if (value <= 0)
                    throw new System.Exception("length must be greater than zero");
                else
                    _length = value;
            }
        }

PostSharp allows us to remove this boilerplate code with contracts. Use NuGet to install postsharp.patterns.common and add your license to the project. Now add some annotations to the code like this...

using PostSharp.Patterns.Contracts;
namespace Box
{
    public class Box
    {
        [StrictlyGreaterThan(0)]
        public int length { get; set; }

        [StrictlyGreaterThan(0)]
        public int width { get; set; }

        [StrictlyGreaterThan(0)]
        public int height { get; set; }

        public Box() { }
        public static Box CreateBoxFromLengthWidthHeight(int length, int width, int height)
        {
            return new Box() { length = length, height = height, width = width };
        }
    }
}

We can use xUnit to test that this works. Use NuGet to add xunit and xunit.runner.visualstudio. Your solution's references should look like this.



Now add a test class to the Box.cs file. I put it inside the Box namespace.

using PostSharp.Patterns.Contracts;
using Xunit;

namespace Box
{
    public class Box
    {
        [StrictlyGreaterThan(0)]
        public int length { get; set; }

        [StrictlyGreaterThan(0)]
        public int width { get; set; }

        [StrictlyGreaterThan(0)]
        public int height { get; set; }

        public Box() { }

        public static Box CreateBoxFromLengthWidthHeight(int length, int width, int height)
        {
            return new Box() { length = length, height = height, width = width };
        }
    }

    public class TestBox
    {
        [Theory]
        [InlineData(1,1,1)]
        [InlineData(100,100,100)]
        [InlineData(0,1,1)]
        [InlineData(1,0,1)]
        [InlineData(1,1,0)]
        public void TestBoxConstructor(int length, int width, int height)
        {
            Box box = Box.CreateBoxFromLengthWidthHeight(length, width, height);
        }
    }
}


If we browse to the test Explorer and run the tests we can see that each of our contracts gets broken in turn.



Of course we really want all our tests to pass so let's rewrite the TestBox class and split it into a set of tests that should pass and a set of tests that should throw an exception...


    public class TestBox
    {
        [Theory]
        [InlineData(1,1,1)]
        [InlineData(100,100,100)]
        public void TestBoxConstructorPass(int length, int width, int height)
        {
            Box box = Box.CreateBoxFromLengthWidthHeight(length, width, height);
        }

        [Theory]
        [InlineData(0, 1, 1)]
        [InlineData(1, 0, 1)]
        [InlineData(1, 1, 0)]
        public void TestBoxConstructorException(int length, int width, int height)
        {
            Box box;
            Assert.Throws<ArgumentOutOfRangeException>(() => box = Box.CreateBoxFromLengthWidthHeight(length, width, height));
        }
    }



No comments:

Post a Comment