Click here to Skip to main content
15,879,239 members
Articles / Programming Languages / C# 4.0

Units of Measure Validator for C#

Rate me:
Please Sign up or sign in to vote.
4.66/5 (28 votes)
2 Jul 2012CPOL7 min read 80.8K   2.4K   41   33
This library provides a MSBuild task for compile time validation of units of measurement within C# code.

Introduction

In C#, the sum of 1 meter and 1 second will be 2. But in most cases, adding or assigning two quantities of different units is a mistake of the programmer, which causes an unexpected and undesired runtime behavior.

This validator checks at compile time for unit violations without adding a new syntax or changing the runtime behavior - only the used units has to be specified through comments or attributes.

Image 1

Figure 1: Validation Sample

Considering the units in figure 1, planet.Speed is meter per second, resultingForce is Newton (kg * m/s^2) and planet.Mass is kilogram. Since the quotient of Newton and kilogram is an acceleration, this quantity cannot be assigned to a speed - a mistake of the programmer, but detected even before starting the whole application.

Usage

The validator is available as MSBuild task and can be included inside any cs-project, whereas a reference at runtime is not needed. The units can be declared within a single project through xml documentation, but for project wide consistence, it is recommend to use the Unit-attribute. In this case, a runtime reference is needed to HDUnitsOfMeasureLibrary.

Installation

First, download either the binaries of this library or compile the library by yourself. All these assemblies should be placed into a suitable folder. 

If you want to install the validator for all projects (not recommended, and you should know what you are doing), you can edit the Microsoft.CSharp.Targets file, otherwise you can install it for each project separately by editing its csproj-file. To do this, open the file with an editor and replace the following comment: 

XML
<!--
<Target Name="BeforeBuild">
</Target>
-->

with this XML:

XML
<UsingTask TaskName="HDLibrary.UnitsOfMeasure.Validator.UnitValidationTask"
    AssemblyFile="Path-To-Your-Installation-Folder\HDUnitsOfMeasureValidatorTask.dll" />
<Target Name="BeforeBuild" DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences">
  <UnitValidationTask Files="@(Compile)" References="@(_ResolveAssemblyReferenceResolvedFiles)" />
</Target>

You should adapt "Path-To-Your-Installation-Folder" to the path of your folder in which you placed your copy of HDUnitsOfMeasureValidatorTask.dll.

That is it - the next time you will build your project, unit violations are displayed within Visual Studio!

First Steps 

By default, the unit of each variable or member is unknown. The behavior of unknown units is the same as without validation. Thus, to work with this validator, the physical elements (like speed, length or time) should be marked with their corresponding units.

Only operations on those marked elements are evaluated.

Declaring Units

For Local Variables

You can declare a unit for a local variable by adding an one-line comment which encloses the unit in square brackets. Between the comment token (//) and the opening bracket, no characters are allowed. After the closing bracket, an explaining text can be added, separated by a whitespace. 

Examples:

C#
int length; //[m] the length in meter
double time; //[s]

For Properties and Fields 

The units of properties and fields can be declared through a xml documentation: 

C#
/// <summary>
/// The length in [m]
/// </summary>
private double length;

/// <summary>
/// The time in [s]
/// </summary>
private double Time {get; set;} 

The unit can be specified anywhere inside the summary-tag and has to be enclosed in square brackets. 

It is of disadvantage, that the unit description is only available for references within this project, because references from other projects currently have no access to the xml documentation and assume the unit of this member as "unknown". 

For this case, you can use the UnitAttribute. Here it is of disadvantage, that you have to add an assembly reference to HDUnitsOfMeasureLibrary.dll, and this dependency still exists at runtime. Nevertheless, using the Unit-attribute is the recommended way:

C#
[Unit("m")]
private double length;

[Unit("s")]
private double Time {get; set;}

For Methods, Constructors and Parameters 

Methods and constructors are parameterized members, which have several input units and zero or one output unit (for constructors, the output unit, if given, determines the unit of the created object). 

If you want to declare the unit within the xml documentation, you can do it the following way: 

C#
/// <param name="time">time ([s])</param>
/// <param name="number">number in [1] - number is a dimensionless quantity</param>
/// <param name="unknown">can be any unit, no unit is specified</param>
/// <returns>the length, [m]</returns>
public int GetLength(int time, int number, double unknown) {...} 

If no unit is specified for any of the parameters, its unit is unknown. 

Unknown Units in contrast to Dimensionless Units 

If no unit is specified, the unit is unknown. Unknown units are equal to dimensionless units, but assigns to unknown quantities are not validated:

C#
double number = 1; //[1] dimensionless unit
double value = 1; //unknown unit
double length = 1.Meter(); //[m] a length in meters

number = value; //is ok
value = number; //is ok
value = length; //is ok, since no validation is done
number = length; //is not ok, since number is "1" and length is "m"

The Vicious Circle of Units 

Since the unit of a hard coded number is dimensionless, you cannot assign it to an element with a different unit than dimensionless. To break this circle, you can insert the comment "/*ignore unit*/" (no whitespaces) exactly after the "=" in the assignment - such assignments are not validated. 

With the predefined constants in the Units<code> class or the extension methods on int and double, you can easily convert any unknown or dimensionless unit into any target unit without using "ignore unit".

C#
int time = 5; //[m] -> not ok, since 5 is unknown ("1") and time is "m"
time =/*ignore unit*/5; -> is ok, since this assignment will not be validated
time = 5 * Units.Meter; //is ok, since Units.Meter is 1 meter
time = 5.Meter(); //is ok
time = 5.AsUnit("m"); //is ok
time = 5.AsUnit("km").ConvertFromTo("km", "m"); //is ok

Validation

The following assignments to elements with a known unit are validated:
=, *=, /=, +=, -=

Arguments in method invocations are validated, if the parameters require certain units.

A validation is successful, if the inferred unit from the expression is equal to the unit of the target element. Two units are equal if they have the same coherent unit (see here, chapter "What are coherent Units?") and the same conversion factor to it. So, "km" is not equal to "m", but "MN" (Mega newton) is equal to "Gg * m / s^2" (Giga gram * meter / square-second).

Dynamic Unit Descriptions

Usually, each unit description is static. If "a" is declared as meter, "a" will be always meter.

But this is not true for class members or methods: If a vector is declared as meter, the length of the vector is meter too. If a vector is declared as Newton, the length of the vector will be Newton.

This genericity can be accomplished by dynamic unit descriptions. Those descriptions can be resolved into concrete units by involving the used context (the target object and the arguments). Such a dynamic unit can be specified with the DynamicUnitAttribute.

There are three kinds of dynamic expressions: The "@" is the only one which can be used with properties and fields. If you declare the length of a vector as "@", the unit of the target object will be inserted. So if the vector is declared as X, its length is X too. You can even build some more complex expressions like "m/@" or "@^2" - depending on the context it will be "m/m" or "m^2" (if the target object is declared as "m"). 

With curly brackets, the unit of the enclosed parameter can be referenced. Square brackets will insert the value of the argument passed to the referenced parameter. If the referenced parameter is a string and has the attribute UnitDescriptionAttribute, the validator will check this argument to be a valid unit string, if the argument is a constant expression. This attribute can define an underlaying unit constraint for this string. If the underlaying unit constraint is "m", valid arguments are "km" or "cm". This constraint can be dynamic, too.

A parameter can be referenced by its zero based index or its name. 

As example, here the signature of the ConvertFromTo method:

C#
[return: DynamicUnit("[targetUnitString]")]
public static double ConvertFromTo([DynamicUnit("[sourceUnitString]")] this double value,
	[UnitString] string sourceUnitString, [UnitString(UnderlayingUnitConstraint = "[sourceUnitString]")] string targetUnitString)

The result unit is the value of the targetUnitString argument. targetUnitString has to be a valid unit string, which has to be the same underlying unit as the sourceUnitString argument.

If value is kilometer, sourceUnitString has to be "km", otherwise the validator logs an error. If sourceUnitString is "km", targetUnitString has to be some kind of length too - e.g. "cm" or "µm".

External Unit Definition and Description 

If you want to introduce a new unit, you can use the assembly attribute UnitDefinitionAttribute

C#
[assembly: UnitDefinition("N", "Newton", "kg * m/s^2")]

This unit is then available for this project and all projects, which reference this project.

If you want to describe the units of an foreign element, you can use the attribute UnitDescriptionAttribute:

C#
[assembly: UnitDescription("System.Windows.Vector.Length", ResultUnit = "@")]

Beside the result unit, you can define the parameter units for parameterized members too. In this case, you can even specify the overloading for this description (e.g. "double,double").

An example:

C#
[assembly: UnitDescription("System.Math.Min", ResultUnit = "{0}", ParameterUnits = ",{0}")]

This description forces the second argument to have the same unit as the first. At the same time it specifies the unit of the result. The first argument can be any unit.

Thus, the resulting unit of Math.Min(1.Meter(), 2.Meter()) will be meter. 

Implementation, a Short Overview

The core libraries of this validator are NRefactory and my HDUnitsOfMeasureLibrary.

The process of validation is separated into different steps, some of them are parallelized.

First, the code is parsed into an abstract syntax tree. Parallel to this, the referenced assemblies are loaded (if not cached) using Mono.Cecil. Then both are converted into a single type-system. This type-system is traversed to find references to the same element and to extract the units from the attributes and xml documentation. After this step, each element has an unit description. An unit description can resolve possible dynamic units into concrete units by analyzing the passed target object and arguments. 

In the next step, the whole abstract syntax tree is traversed again (but now parallelized) to validate assignments and method calls by using the extracted units and the inferred units of expressions. 

Future 

This library is still under development - there are still some bugs and limitations.

Currently, one big issue is the performance: Despite parallelization, it is still quite slow - but there are enough points to optimize.

History 

  • 1.0 Initial version 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Student
Germany Germany
Presently I am a student of computer science at the Karlsruhe Institute of Technology in Germany.

Comments and Discussions

 
QuestionGread idea, but I can't get it to work properly. Pin
Member 959045511-Nov-12 5:52
Member 959045511-Nov-12 5:52 
GeneralRe: Gread idea, but I can't get it to work properly. Pin
Henning Dieterichs11-Nov-12 10:57
Henning Dieterichs11-Nov-12 10:57 
QuestionAssign unit definition to items of list Pin
Thecentury3-Sep-12 22:52
Thecentury3-Sep-12 22:52 
BugNull Reference Exception on build Pin
Thecentury3-Sep-12 22:37
Thecentury3-Sep-12 22:37 
GeneralMy vote of 5 Pin
Jean Paul V.A13-Jul-12 13:48
Jean Paul V.A13-Jul-12 13:48 
GeneralMy vote of 5 Pin
ShlomiO11-Jul-12 9:22
ShlomiO11-Jul-12 9:22 
GeneralMy vote of 4 Pin
Derndorfer11-Jul-12 1:09
Derndorfer11-Jul-12 1:09 
GeneralRe: My vote of 4 Pin
Henning Dieterichs11-Jul-12 1:26
Henning Dieterichs11-Jul-12 1:26 
Thank you for your comment!

But you haven't tried the attached demo, have you?
If the demo is correct, how the validator, which checks for mistakes, would be demonstrated?
-> This mistake is intended.

Quote:
Error 4 "kg * m / s^2" expected, but got "m^3". C:\Users\Henning Dieterichs\Desktop\Validator_Source\Validator\ValidatorDemo\ConsoleApplicationValidatorDemo\PlanetSystem.cs 38 39 ConsoleApplicationValidatorDemo



-- edit: See my next comment.
If you find spelling- or grammer-mistakes, please let me know, so that I can correct them (at least for me) - english is not my first language...


modified 11-Jul-12 12:50pm.

GeneralRe: My vote of 4 Pin
Derndorfer11-Jul-12 1:34
Derndorfer11-Jul-12 1:34 
GeneralRe: My vote of 4 Pin
Henning Dieterichs11-Jul-12 2:12
Henning Dieterichs11-Jul-12 2:12 
GeneralMy vote of 5 Pin
Galatei9-Jul-12 9:07
Galatei9-Jul-12 9:07 
GeneralMy vote of 5 Pin
eran20223-Jul-12 8:48
professionaleran20223-Jul-12 8:48 
QuestionValidation vs New Types Pin
Paulo Zemek3-Jul-12 8:04
mvaPaulo Zemek3-Jul-12 8:04 
GeneralRe: Validation vs New Types Pin
Henning Dieterichs3-Jul-12 12:29
Henning Dieterichs3-Jul-12 12:29 
GeneralRe: Validation vs New Types Pin
Paulo Zemek3-Jul-12 12:34
mvaPaulo Zemek3-Jul-12 12:34 
GeneralRe: Validation vs New Types Pin
Henning Dieterichs4-Jul-12 1:29
Henning Dieterichs4-Jul-12 1:29 
GeneralRe: Validation vs New Types Pin
Henning Dieterichs5-Jul-12 11:31
Henning Dieterichs5-Jul-12 11:31 
GeneralRe: Validation vs New Types Pin
Paulo Zemek5-Jul-12 11:55
mvaPaulo Zemek5-Jul-12 11:55 
GeneralRe: Validation vs New Types Pin
Qwertie10-Jul-12 10:24
Qwertie10-Jul-12 10:24 
GeneralRe: Validation vs New Types Pin
Henning Dieterichs10-Jul-12 11:15
Henning Dieterichs10-Jul-12 11:15 
GeneralRe: Validation vs New Types Pin
Qwertie10-Jul-12 19:55
Qwertie10-Jul-12 19:55 
GeneralRe: Validation vs New Types Pin
Qwertie11-Jul-12 4:14
Qwertie11-Jul-12 4:14 
GeneralRe: Validation vs New Types Pin
Henning Dieterichs11-Jul-12 11:48
Henning Dieterichs11-Jul-12 11:48 
Questionnice Pin
BillW333-Jul-12 7:31
professionalBillW333-Jul-12 7:31 
GeneralMy vote of 5 Pin
ashved3-Jul-12 4:13
ashved3-Jul-12 4:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.