With a new job comes new challenges, a new way of thinking, and different obstacles to overcome. My first post-university job is here. I did a work placement during my degree which taught me a lot about programming fundamentals and software design. Yet after just two months, this place has already made me question a lot of what I learned and provided me with the opportunity to approach challenges in new and creative ways. I would like to share one of the creative solutions I picked up – known as preprocessor directives.
C# and the .NET framework
When it comes to C# design, there are many different versions of the .NET framework that we need to consider. Many features available in the more up-to-date frameworks simply aren’t as easy to implement in older versions. This can provide unique challenges when faced with legacy systems, as you must design within the constraints of what is available. I was recently given a case with just that scenario for our Babel service, which runs in .NET 2.0. This version of the .NET framework had an issue I was interested in solving. To achieve this, I needed to run code in two different .NET versions – a later version, to gather and store some information, and .NET 2.0, to unpack and apply the information so it could be used by software.
I myself am a lazy programmer; the idea of building two separate applications to achieve this seemed a little tiresome. So, I began considering different options. Was it possible to only run certain code in a project depending on the .NET version currently in use by the project? The short answer is yes!
For the long answer, I’m going to explain how you can do this using a little something called a preprocessor directive – a way of communicating with the compiler to achieve some neat shortcuts.
What is a preprocessor directive?
A preprocessor directive is a way of communicating with the compiler that allows you to add conditional statements at compile time, dependent on execution environments. In layman’s terms, you can conditionally compile code depending on settings you have chosen. A simple (and very useful) example of this is the Debug symbol. If you are running your code under Debug mode, then any code told to compile under Debug will compile and execute, however anything that isn’t in Debug won’t.
The Debug symbol is useful when writing test code that you don’t want to be compiled in a release system.
Notice how parts of the code are grey? This means that the code won’t compile when you run the build on it!
A preprocessor directive can be declared in two places: the code itself (locally), or in the .csproj file (globally). The Debug symbol is global and tied to the Debug setting, meaning that any changes to the .csproj will reflect in your code.
This becomes very useful when approaching the challenge, I was first presented with – different code that needs to be run in two different .NET versions. To achieve this, we can pop open the .csproj file and make a few small changes. By adding a new PropertyGroup to our project, we can define our own conditional symbols to react to the .NET version the code is compiling in.
The bottom two lines tie in the CustomConstants we just made into the project so they can be used alongside the ones defined by configuration.
This looks scarier than it is. All we have done here is created some CustomConstants in a new PropertyGroup at the bottom of our file and then tied them in to the existing constants with the last two lines. Each of our CustomConstants is simply checking for the TargetFrameworkVersion variable to see if it matches a particular version, and defining the symbol if it is.
You can see the program is currently targeting .NET 2.0.
What’s the point?
Great, we have some fancy looking grey code in some places. What’s the point of this?
Well, let’s consider some benefits:
- Using symbols like Debug allows us to write code that is usable only in test environments. We could print statements, apply extra logging, or change settings at startup using this.
- Tying symbols to a .NET framework would allow us to write new and flexible code in solutions while maintaining backwards compatibility with older versions of the framework. There are a lot of things available in later versions of the framework which simply aren’t in older versions. The ability to upgrade code efficiently and maintain backwards compatibility is valuable.
- The two examples I’ve given aren’t the only things you can tie them to. Anything the compiler knows in advance can be used for this!
Preprocessor directives are an interesting and useful way to conditionally compile code. They aren’t just limited to the example above – any information that the compiler uses when compiling code could potentially be used to provide conditional code. This can provide great benefits when applied correctly and offers unique solutions to some problems!