Do you have some sort of ImmutableAttribute in your domain that you use to mark classes as immutable? Have you ever needed to enforce that contract? Checking for readonly fields isn’t enough? Well, this weekend I had a code spike that helped solve this problem in my current project.
For this project, I’m using the NoRM driver for MongoDB, and one of the limitations of the serializer is that all types must be classes, must have a default constructor, and all properties have a public setter. So, now the domain has a bunch of classes like this:
public class UserCreatedEvent : IEvent{public string Name { get; set; }public UserCreatedEvent() { }
public UserCreatedEvent(string name) { Name = name; }}
That God for code snippets (or Resharper templates). With so many classes like this that need to get serialized, I wanted to extra sure that no code ever calls the setter method for the Name property. Thankfully, with some help of Mono.Cecil, it’s possible.
First off, you need to define ImmutableAttribute and that add that do classes, and in my case, it is historical domain events that get serialized to an event store.
Then, you just write a unit test which leverages the power of Mono.Cecil. It turned out to be pretty simple. Here’s the code:
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using NUnit.Framework;
namespace blingcode
{[TestFixture]public class ImmutabilityTests{private static readonly MethodDefinition[] _setterMethods;private static readonly AssemblyDefinition[] _assemblies;static ImmutabilityTests()
{_assemblies = new[]
{AssemblyDefinition.ReadAssembly(typeof(Something).Assembly.Location),
};_setterMethods = _assemblies.SelectMany(a => a.Modules).SelectMany(m => m.Types).Where(t => t.CustomAttributes.Any(attr => attr.AttributeType.Name.Contains("ImmutableAttribute")))
.SelectMany(t => t.Properties).Where(p => p.SetMethod != null)
.Select(m => m.SetMethod).ToArray();}[Test]public void ClassesWith_ImmutableAttribute_ShouldNotUse_PropertySetters(){AssertForViolations(_assemblies.SelectMany(a => a.Modules).SelectMany(m => m.Types).Where(t => t.IsClass).SelectMany(t => t.Methods));}[Test]public void ThisFixtureActuallyWorks(){var assembly = AssemblyDefinition.ReadAssembly(typeof(ImmutabilityTests).Assembly.Location);
var type = assembly.Modules.SelectMany(m => m.Types).Where(t => t.IsClass && t.FullName.Contains(GetType().FullName)).First();try
{AssertForViolations(type.Methods);}catch (AssertionException)
{Assert.Pass();}}private static void AssertForViolations(IEnumerable<MethodDefinition> potentialMethods){foreach (var method in potentialMethods.Where(m => m.HasBody)){foreach (Instruction ins in method.Body.Instructions.Where(ins => ins.OpCode == OpCodes.Callvirt)){MemberReference mr = ins.Operand as MemberReference;
if (mr != null){var result = _setterMethods.FirstOrDefault(m => m.FullName == mr.FullName);if (result != null){throw new AssertionException(result + " was invoked by " + method + ", even though the type has the Immutable attribute.");}}}}}private void InvokeCardSetters(){// this only exists to test that the test does indeed work
var c = new SomeImmutableClass();
c.SomeImmutableValue = 123;}}}
Nothing too complicated. The main thing to look for is the callvirt method, which the C# compiler always generates for classes. Then, you match the operand to the method definition and viola!
No comments:
Post a Comment