Skip to content

Reflection in C# scripts

I have been using C# scripting as a tool for writing build scripts for many years now. At first it was ScriptCs using Sublime Text as the editor. There was no intellisense or debugging capabilities, but still it was insanely powerful to have all the sweetness of C# available in a simple script file. In addition to using C# scripts in a variety of ways, I am also responsible for a handful of pull requests over at the dotnet-script repo. Together, Filip, my self and several other contributors have created a C# script runner that runs on .Net Core with support for inline NuGet packages, debugging and much more. You should check it out 😀

This post is not really about the evolution of C# scripting, but rather about how I went about creating dotnet-steps which is a super simple way of composing "steps" in a C# script.

"A small step for man kind, but a HUGE leap for build scripts"

For a build script it is pretty common to have various steps such as build, test, pack, deploy and so forth. These steps can then be composed together to form the flow of the build script and we can also execute the script in such a way that we can cherrypick the step(s) to be executed.

There are plenty of tools that provides similar concepts.

  • Cake - Full fledged build system with all batteries included.
  • Bullseye - Clean and simple "targets" runner written as a console application.
  • Nuke - Build scripts written as console application

These tools all have their own sort of DSL to declare and chain composable chucks of code together.

Cake

Task("Run-Unit-Tests")
    .IsDependentOn("Build")
    .Does(() =>
{
    NUnit("./src/**/bin/" + configuration + "/*.Tests.dll");
});

Bullseye

Target("default", DependsOn("drink-tea", "walk-dog"));
Target("make-tea", () => Console.WriteLine("Tea made."));
Target("drink-tea", DependsOn("make-tea"), () => Console.WriteLine("Ahh... lovely!"));
Target("walk-dog", () => Console.WriteLine("Walkies!"));

Nuke

Target Compile => _ => _
    .Executes(() => { });

Target Pack => _ => _
    .DependsOn(Compile)
    .Executes(() => { });

Target Test => _ => _
    .DependsOn(Compile)
    .Executes(() => { });

Target Full => _ => _
    .DependsOn(Pack, Test);

My personal favourite among these three is the Nuke syntax. There are no use of strings to declare targets or to reference target dependencies. It is just plain and simple C# in a type safe manner.

Question is, can we do even simpler? Written as a C# script without the need for a host console application or a special runner.

The syntax I was aiming for was something like

Step Compile = () = WriteLine(nameof(Compile));

Step Pack = () => 
{
    Compile();
    WriteLine(nameof(Pack));
}

Step Test = () => 
{
    Compile();
    WriteLine(nameof(Test));
}

Step Full = () =>
{
    Pack();
    Test();
}

await ExecuteSteps(Args);

As we can see there is no special DSL in this syntax. A "Step" is just a delegate where the body of the method becomes extremely simple. Dependencies to other steps are specified by just calling the steps we depend upon.

The step itself is represented by a delegate public delegate void Step() and dotnet-steps reflects over these delegates and figures out what step(s) to execute based on the arguments passed to the script.

Reflecting "this"

As we can see in the dotnet-steps example there is no class that contains the steps. They are written as top level fields in the script. That is a very powerful aspect of C# scripting in general. We can just start to write code without any class containing it.

The first challenge here is obtaining a list of available steps since we don't really have a class type for which we can use as a starting point for reflection. We can't use this either since C# scripting does not allow the this keyword in the top level portion of a script.

So where does this top level step fields end up? They don't belong to a class ...or do they?

It is a matter of fact that they actually do. It is just hidden for us. Top level members gets compiled as members of a class name sumbisson#0 which from the compilers point of view is just normal C# code. But how do we get access to this type and preferably also the instance of "this" represented as an instance of submission#0?

The trick here is to use a top level lambda to provide this information to us.

Action stepsDummyaction = () => StepsDummy();
void StepsDummy() { }

This stepsDummyAction is also a top level field so that it belongs to submission#0 as well. It is not static so it must have a target, right? Let's try this.

Action stepsDummyaction = () => StepsDummy();
void StepsDummy() {}
WriteLine(stepsDummyaction.Target.GetType());

And the result

Submission#0

Success! We now have access to Submission#0 instance through the Target property of our dummy action. And we have access to the Submission#0 type which again makes it possible to reflect over Step fields declared in the Submission#0 type.

So now we can write code like

private static FieldInfo[] GetStepFields<TStep>()
{
    return _submissionType.GetFields().Where(f => f.FieldType == typeof(TStep)).ToArray();
}

In order to get the actual Step delegate we need to get the value from each Step field. Unless the field is declared as static, we also need the Submission#0 instance to get the field value.

private static TStep GetStepDelegate<TStep>(FieldInfo stepField)
{
    return (TStep)(stepField.IsStatic ? stepField.GetValue(null) : stepField.GetValue(_submission));
}

Note: These methods are generic so that they can work for different delegate types. We also have an AsyncStep for dealing with async steps.

Executing steps

Now that we have access to the Step delegates, the next part is executing them which is pretty trivial by itself, but we also need to track the duration of each step so that we can present a nice summary report at the end of execution.

Consider the following two steps.

Step step1 = () => WriteLine(nameof(step1));

Step step2 = () => 
{
    step1();
    WriteLine(nameof(step1));
};

If we execute step2 we will first execute step1 and then the rest of the code in step2. And remember that we still want to have step1 appear in the summary report as a separate entry with its own duration measurement.

---------------------------------------------------------------------
Steps Summary
---------------------------------------------------------------------
Step                Duration           Total
-----               ----------------   ----------------
step1               00:00:00.0009179   00:00:00.0009179
step2               00:00:00.0008100   00:00:00.0017279
---------------------------------------------------------------------
Total               00:00:00.0017279

How can we possibly intercept the call to step1 from within step2 ?

Decorating Steps

Remember that these Step delegates are just instance fields on the underlying Submission#0 instance? So what if we set these Step field values to another Step. A Step that wraps the original Step around a Stopwatch?

Step wrappedSted = () = 
{
    var stopWatch = Stopwatch.StartNew();
    step(); // This is the original step declared in the script
    stopWatch.Stop();
}

All we really need to do now is to set the field value to this wrapped Step instead.

var stepFields = GetStepFields<Step>();
foreach (var stepField in stepFields)
{
    var step = GetStepDelegate<Step>(stepField);
    Step wrappedStep = () =>
    {
        StepResult stepresult = PushStepResultOntoCallStack(stepField);
        var stopWatch = Stopwatch.StartNew();
        step();
        stopWatch.Stop();
        PopCallStackAndUpdateDurations(stepresult, stopWatch);
    };
    stepField.SetValue(stepField.IsStatic ? null : _submission, wrappedStep);
}

Okay, what is this PushStepResultOntoCallStack and PopCallStackAndUpdateDurations methods?

First of all, the StepResult class is just a simple class that contains information about the executed step.

public class StepResult
{
    public StepResult(string name, TimeSpan duration, TimeSpan totalDuration)
    {
        Name = name;
        Duration = duration;
        TotalDuration = totalDuration;
    }

    public string Name { get; }
    public TimeSpan Duration { get; set; }
    public TimeSpan TotalDuration { get; set; }
}

So for each executed step, we record the Duration which is the time spent in a step excluding the time spent calling other steps. The TotalDuration is the time spent executing the step including the time spent calling other steps.

For this to work we need some kind of call stack so that we can keep track of the exclusive time being spent in each step.

private static StepResult PushStepResultOntoCallStack(FieldInfo stepField)
{
    var stepresult = new StepResult(stepField.Name, TimeSpan.Zero, TimeSpan.Zero);
    _callStack.Push(stepresult);
    return stepresult;
}

When the step has executed, we pop the current StepResult off the stack and update the duration of the calling step.

private static void PopCallStackAndUpdateDurations(StepResult stepresult, Stopwatch stopWatch)
{
    var durationForThisStep = stopWatch.Elapsed;
    stepresult.TotalDuration = durationForThisStep;
    _results.Add(_callStack.Pop());

    if (_callStack.Count > 0)
    {
        var callingStep = _callStack.Peek();
        callingStep.Duration = callingStep.Duration.Subtract(durationForThisStep);
    }
    stepresult.Duration = stepresult.Duration.Add(durationForThisStep);
}

Summary

The goal of this post was mainly to show how to do reflection in C# scripts and the technique I've shown here can of course be used in a number of interesting ways. Check out the documentation over at the dotnet-steps for more details. If you have any questions or ideas how to improve, don't hesitate to contact me.