Exploring the .NET 4.0 framework in C#: System.Diagnostics.Contracts
Tightly-coupled, loosely bound. This has been my mantra for enterprise software development for the past 7 or so years. I have always been in favor of compile time over run time errors at (nearly) any cost. I don’t feel that I am dogmatic about it, I understand the need for flexibility in internal interfaces and early in a projects life cycle. But, at the point you have down stream systems that depend on your application to perform, the rules change. You are reading this entry using an application that has a loosely coupled, forgiving interface with the content it receives for rendering (see Joel’s discussion of inherent folly of this choice http://www.joelonsoftware.com/items/2008/03/17.html), so large systems don’t require the level of synchronization I prefer to function. I believe, however, they require it to function well.
Apparently, I am not alone in my belief. The .NET 4.0 framework now has an mscorlib level component that helps enforce the coupling at compile time. The System.Diagnostics.Contracts namespace contains a suite of static classes that, coupled with a post-processing code re-writer, provides static and run time implementations of preconditions, post-conditions, and invariants. In this post I am going to start with some simple examples of how the syntax works. In future posts, I hope to delve into the implications of this system as well as how our best practices need to be updated to take advantage of the power of these structures.
The first thing you need to do is to install the new contract tool set as outlined in http://blogs.msdn.com/bclteam/archive/2010/01/26/i-just-installed-visual-studio-2010-now-how-do-i-get-code-contracts-melitta-andersen.aspx. Please note that unless you are using Premium or Ultimate, you will not get Static evaluation, only run time.
You then turn static checking on in the settings in Project Properties > Code Contracts and select “Perform Static Contract Checking” so it looks like this:
Through static checking, we should see compile time errors. Unfortunately, the combination of VS 2010 RC I am using along with the Release 1.2.30312.0 of the Code Contracts for .NET doesn’t have the static version working properly. So for this example we are going to have to look at run time errors. That is set on the same Project Property page
Let's start with a simple sample program with some properties.
class Program
{
public string Name { get; set; }
public int Age { get; set; }
static void Main(string[] args)
{
Program program = new Program();
program.Age = -7;
program.Name = "jon";
}
}
Within the program above, we can see a common pattern for properties. If we look at the underlying IL of one of the setter methods, we see what we should expect:
.method public hidebysig specialname instance void
set_Age(int32 'value') cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld int32 Contracts.Program::'k__BackingField'
IL_0007: ret
} // end of method Program::set_Age
After modifying the code to include the required using statement:
using System.Diagnostics.Contracts;
and updating the property to have the following contract statement:
public int Age
{
get { return _age; }
set
{
Contract.Requires(value >= 0);
_age = value;
}
}
When a command line program is run with a bad argument, the following exception is thrown:
Unhandled Exception: System.Diagnostics.Contracts.__ContractsRuntime+ContractException: Precondition failed: value >= 0
at System.Diagnostics.Contracts.__ContractsRuntime.TriggerFailure(ContractFailureKind kind, String message, String userMessage, String conditionText, Exception inner) in :line 0
at System.Diagnostics.Contracts.__ContractsRuntime.ReportFailure(ContractFailureKind kind, String message, String conditionText, Exception inner) in :line 0
at System.Diagnostics.Contracts.__ContractsRuntime.Requires(Boolean condition, String message, String conditionText) in :line 0
at Contracts3.Program.set_Age(Int32 value) in c:ContractsProgram.cs:line 19
at Contracts.Program.Main(String[] args) in c:ContractsProgram.cs:line 33
and looking at the IL I find where the rewrite has added wrappers to handle the pre-conditions:
.method public hidebysig specialname instance void
set_Age(int32 'value') cil managed
{
// Code size 74 (0x4a)
.maxstack 3
IL_0000: ldsfld int32 System.Diagnostics.Contracts.__ContractsRuntime::insideContractEvaluation
IL_0005: ldc.i4.4
IL_0006: bgt IL_003c
.try
{
IL_000b: ldsfld int32 System.Diagnostics.Contracts.__ContractsRuntime::insideContractEvaluation
IL_0010: ldc.i4.1
IL_0011: add
IL_0012: stsfld int32 System.Diagnostics.Contracts.__ContractsRuntime::insideContractEvaluation
IL_0017: ldarg.1
IL_0018: ldc.i4.0
IL_0019: clt
IL_001b: ldc.i4.0
IL_001c: ceq
IL_001e: ldnull
IL_001f: ldstr "value >= 0"
IL_0024: call void System.Diagnostics.Contracts.__ContractsRuntime::Requires(bool,
string,
string)
IL_0029: nop
IL_002a: leave IL_003c
} // end .try
finally
{
IL_002f: ldsfld int32 System.Diagnostics.Contracts.__ContractsRuntime::insideContractEvaluation
IL_0034: ldc.i4.1
IL_0035: sub
IL_0036: stsfld int32 System.Diagnostics.Contracts.__ContractsRuntime::insideContractEvaluation
IL_003b: endfinally
} // end handler
IL_003c: nop
IL_003d: ldarg.0
IL_003e: ldarg.1
IL_003f: stfld int32 Contracts3.Program::_age
IL_0044: br IL_0049
IL_0049: ret
} // end of method Program::set_Age
The rewriting is being done by ccrewrite, and can be conditionally compiled in. There is also a structure set up to allow runtime code to catch the contract exception to be handled internally.
links: http://msdn.microsoft.com/en-us/library/system.diagnostics.contracts(v=VS.100).aspx