AOP Contracts with PostSharp
So, I’ve been using Aspect Oriented Programming for a while. My company has a license for PostSharp. Recently I started using it more, in particularly, I started using the Contracts feature for checking the parameters of my methods. This is called precondition checking. Read more here: PostSharp Contracts
Here is a basic example of precondition checking. Imagine a Person class where the firstName and lastName should throw an exception if null, empty, or whitespace.
Your code might look like this:
using System; namespace CustomContractsExample { public class Person { private readonly string _FirstName; private readonly string _LastName; public Person(string firstName, string lastName) { // Validate first name if (string.IsNullOrWhiteSpace(firstName)) throw new ArgumentException("Parameter cannot be a null, empty or whitespace string.", "firstName"); // Validate last name if (lastName == string.Empty) throw new ArgumentException("Parameter cannot be an null, empty or whitespace string.", "lastName"); // Initialize fields _FirstName = firstName; _LastName = lastName; } public string FirstName { get { return _FirstName; } } public string LastName { get { return _LastName; } } } }
Ugh! Those lines to validate and throw exceptions are UGLY! Not to mention redundant. How many times might you write this same code. Probably over and over again. Of course this breaks the Don’t Repeat Yourself (DRY) principle.
Now, install PostSharp.Patterns.Model from NuGet.
(Note 1: This also installs PostSharp and PostSharp.Patterns.Common.)
(Note 2: This requires a paid license but is well worth it).
Looking at the Contracts, we particularly are interested in this one:
RequiredAttribute
RequiredAttribute is an attribute that, when added to a field, property or parameter, throws an ArgumentNullException if the target is assigned a null value or an empty or white-space string.
Here is the same class using PostSharp.Aspects.Contracts.
using PostSharp.Patterns.Contracts; namespace CustomContractsExample { public class Person { private readonly string _FirstName; private readonly string _LastName; public Person([Required]string firstName, [Required]string lastName) { _FirstName = firstName; _LastName = lastName; } public string FirstName { get { return _FirstName; } } public string LastName { get { return _LastName; } } } }
Now, doesn’t that looks much nicer.
Yes, it does.
One issue:
It throws an ArgumentNullException if the string is empty or WhiteSpace. To me this is “Ok” but not preferred. If the data is not null, we shouldn’t say it is. Looking at the RequiredAttribute code, it is doing this:
public Exception ValidateValue(string value, string locationName, LocationKind locationKind) { if (!string.IsNullOrWhiteSpace(value)) return null; return CreateArgumentNullException(value, locationName, locationKind); }
It really should be doing this.
public Exception ValidateValue(string value, string locationName, LocationKind locationKind) { if (value == null) return (Exception)this.CreateArgumentNullException((object)value, locationName, locationKind); if (string.IsNullOrWhiteSpace(value)) return (Exception)this.CreateArgumentException((object)value, locationName, locationKind); return (Exception)null; }
We can easily roll our own class to fix this bug.
using System; using PostSharp.Aspects; using PostSharp.Patterns.Contracts; using PostSharp.Reflection; namespace CustomContractsExample { public sealed class StringRequiredAttribute : LocationContractAttribute, ILocationValidationAspect<string> { protected override string GetErrorMessage() { return (ContractLocalizedTextProvider.Current).GetMessage("RequiredErrorMessage"); } public Exception ValidateValue(string value, string locationName, LocationKind locationKind) { if (value == null) return CreateArgumentNullException(value, locationName, locationKind); if (string.IsNullOrWhiteSpace(value)) return CreateArgumentException(value, locationName, locationKind); return null; } } }
And here are my unit tests for what I expect. They all pass with my new StringRequired class.
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using CustomContractsExample; namespace CustomContractsExampleTests { [TestClass] public class PersonTests { // Arrange private const string Firstname = "Jared"; private const string LastName = "Barneck"; [TestMethod] public void TestNewPersonWorks() { var person = new Person(Firstname, LastName); Assert.IsNotNull(person); Assert.IsFalse(string.IsNullOrWhiteSpace(person.FirstName)); Assert.IsFalse(string.IsNullOrWhiteSpace(person.LastName)); } [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void TestNewPersonThrowsExceptionIfFirstNameNull() { new Person(null, LastName); } [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void TestNewPersonThrowsExceptionIfLastNameNull() { new Person(Firstname, null); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void TestNewPersonThrowsExceptionIfFirstNameEmpty() { new Person(string.Empty, LastName); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void TestNewPersonThrowsExceptionIfLastNameEmpty() { new Person(Firstname, ""); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void TestNewPersonThrowsExceptionIfFirstNameWhiteSpace() { new Person(" ", LastName); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void TestNewPersonThrowsExceptionIfLastNameWhiteSpace() { new Person(Firstname, " "); } } }
Powered by PostSharp