Scoop! C#’s new #until directive

(Disclaimer: This post is about a C# language feature I’d like to see, not one that actually exists. Once the feature gets added, the title will be accurate and I’ll be able the world’s most pro-active blogger. :))

Update 1: Added another approach

I’m trying to evolve a framework here at my current client. There are some 30+ solutions and an unknown (to me) number of developers dependent upon this framework. As such, I can’t go and make breaking changes without everybody’s CI build dieing and me getting escorted from the building.

Even when I do have access to all the code in one solution, I prefer a three pass approach of:

  1. implementing new functionality and bridging old functionality but marking it obsolete
  2. cleaning up all the build warnings triggered by the [Obsolete] attributes
  3. going back and deleting the obsolete code now that nothing depends on it anymore

Starting with that approach, I have some code like this:

[Obsolete("Use SomeOtherProperty instead.")]
public SomeType SomeProperty { get; set; }

public SomeOtherType SomeOtherProperty { get; set; }

In theory, each team will then drive down their build warnings over time.

In practice, I’ve never seen people do this very excitedly and I have no hope of driving all these down myself.

What I want to do is offer a fixed grace period something like this:

[Obsolete("Use SomeOtherProperty instead. This member will be removed on 1st Aug 2010.")]
public SomeType SomeProperty { get; set; }

public SomeOtherType SomeOtherProperty { get; set; }

This gives each team a known grace period to update their usages, and then forces them after that. (It’s basically two release cycles.)

The problem is that I want to have these members die automatically once this date arrives. (I may or may not be here, etc.)

Approach #1

I came up with this:

#until 2010-08-01
    [Obsolete("Use SomeOtherProperty instead. This member will be removed on 1st Aug 2010.")]
    public SomeType SomeProperty { get; set; }
#enduntil

public SomeOtherType SomeOtherProperty { get; set; }

Basically, that code block is only compiled up until 1st August. As soon as that date ticks around, the member is magically nuked from the build and the lazy downstream users start getting compile errors. The simple syntax of this also makes it easy for me to run some PowerShell + regular expressions over the framework codebase on a regular basis and remove the actual source code.

Unfortunately, C# doesn’t include the #until directive yet and I doubt Anders is going to give me a custom compiler build any time soon. 🙂

Approach #2

My next idea was to create a numeric version of the date and then using a basic conditional compilation directive:

#if DATESERIAL < 20100801
    [Obsolete("Use SomeOtherProperty instead. This member will be removed on 1st Aug 2010.")]
    public SomeType SomeProperty { get; set; }
#endif

public SomeOtherType SomeOtherProperty { get; set; }

I’d then include something in the build script that adds the current date as a symbol (eg. csc.exe /define:DATESERIAL=20100801).

Unfortunately, symbols are just symbols (duh) and thus don’t have values. Also, the pre-processor ‘expressions’ used in #if only support basic boolean expressions.

Approach #3

My next idea was to make the dates less granular and define a series of symbols for the last 3 months or so. For example, a build run today 17th June 2010 would be executed like so – with a symbol for April, May and June:

csc.exe /define:OBSOLETE_201004;OBSOLETE_201005;OBSOLETE_201006

The code could then look like this:

#if OBSOLETE_201005
    [Obsolete("Use SomeOtherProperty instead. This member will be removed on 1st Aug 2010.")]
    public SomeType SomeProperty { get; set; }
#endif

public SomeOtherType SomeOtherProperty { get; set; }

As soon as September ticks around, the OBSOLETE_201005 symbol will fall off the list and voila, the member dies.

This approach is basically flagging the date that we marked something obsolete (in this example, May 2010) and then allowing the build process to determine which ones are in and which ones are one.

I don’t like the approach for a few reasons:

  • it means that the directive isn’t as clear (it’s the date we indicated the change, not the date it’s going to take effect)
  • the message in the attribute can potentially become wrong (say we decided to include four months’ worth of obsolete changes instead of three, all the messages will now be out by one month)
  • all of the members are now forced on to the same attrition cycle – I can’t spread the ‘easier’ ones on to one cycle and the ‘harder’ ones on to a longer cycle

Approach #4

Let’s go back and evolve the syntax from approach #1:

//#until 2010-08-01
    [Obsolete("Use SomeOtherProperty instead. This member will be removed on 1st Aug 2010.")]
    public SomeType SomeProperty { get; set; }
//#enduntil

public SomeOtherType SomeOtherProperty { get; set; }

All I’ve done is add the comment indicator to the start of each of the directives so that they still look like directives but the compiler doesn’t try and process them.

I was already planning to have a PowerShell script that I could use to find the stale code after it had passed its used by date. Keeping the syntax simple makes it easy to find the blocks via regular expressions, so this would be quite easy to do.

I could run this same script at the start of the build:

  1. Create a workspace for the build
  2. Run the PS script across it to remove any expired code
  3. Run the compiler

The problem with this approach is that it clobbers your code. This works fine on a build server where you’re creating a new workspace for every build. It doesn’t work so well in your local environment, and that’s just yucky. This doesn’t affect the downstream consumers (they only get binaries) however it kind of sucks for the framework team.

Approach #5

(This is inspired from Simon’s response.)

Bringing this all back into C#, we can move the onus on to the framework team. First up, lets add a custom attribute to the member:

[ValidUntil(2010, 08, 01)]
[Obsolete("Use SomeOtherProperty instead. This member will be removed on 1st Aug 2010.")]
public SomeType SomeProperty { get; set; }

public SomeOtherType SomeOtherProperty { get; set; }

Now, the framework build could include a unit test that uses reflection to find all the instances of this attribute and evaluate the dates. If the date is in the past, the unit test fails and the framework build fails. The framework team would then identify the build break and delete the now expired code.

Approach #6

Feel free to suggest. 🙂

4 comments

  1. Well, NDepend let’s you do stuff like this too. The good thing about NDepend is that enables people that care to quickly browse the code for a health check of the framework.

  2. You could use approach #3, and have your build server inject the compiler directive. Although not really that much better.

    Or you could write your own compiler which adds the #until compiler directive, sure that can’t be too hard :P.

    Another thought is that you have your build server analyze the C# files before they are compiled, use Regex to find //#until comments and then update the C# file with an ObsoleteAttribute which has the IsError set to true.
    Then you have to have some way to check that back into your source control once the build is completed.

    Yeah, writing your own compiler is probably the cleanest solution 😉

  3. I like approach #5 except instead of a unit test, I would use FxCop. This way, you get an FxCop error for each violation (easier to track down and fix) instead of having to analyze a unit test, figure out what failed, rerun the unit test, repeat. Of course, this assumes FxCop errors = build breaks in your team.

    There’s also ConditionalAttribute but I don’t think it would work well here. E.g. It can only be used on methods (and types derived from Attribute).

  4. I had thought about this in the past before. It would be really simple to accomplish if the ObsoleteAttribute was not sealed by extending the attribute with a date parameter that would indicate when to switch from a warning to an error.

Comments are closed.