Static and dynamic conditions in C#/Nuke build pipelines
Sometimes you run into a little open-source project that makes you wonder how you ever lived without it. Nuke, the C#-based build pipeline framework, is definitely one of them. If you haven’t seen Nuke yet, check out my article for a in-depth explanation. For now, consider the following excerpt.
class Build : NukeBuild
{
public static int Main() => Execute<Build>(x => x.TargetA);
[Parameter] string AzureToken
Target Prepare => _ => _
.Requires(() => AzureToken)
.Executes(() =>
{
// Do something useful with SomeParameter
});
Target Deploy => _ => _
.DependsOn(Prepare)
.Executes(() =>
{
// Do something else
});
This is a simple example in which target Deploy
has a dependency on target Prepare
. When you tell Nuke to execute Deploy
through build.ps1
(or nuke
if you’ve installed it as global tool), it will make sure that Prepare
is executed before Deploy
. And since Prepare
has a required parameter, SomeParameter
, it’ll will make sure it has a non-null
value. If not, it will either throw, or if you’re running the script from a command prompt, prompt you to enter a value.
Now, imagine that you want to execute a target only when a certain condition is met. For example, when you have another target that uses runtime logic to set a field to a certain value that another target should depend on:
bool HasChanges;
Target DetermineChanges => _ => _
.Executes(() =>
{
HasChanges = // execute some run-time logic to set this field
});
There’s actually two ways to make a target conditionally dependent on the value of HasChanges
. One is called OnlyWhenStatic
and the other OnlyWhenDynamic
. If you’ve been building non-trivial build scripts using Nuke, at some point you must have wondered what the subtle difference is between those two. To understand that, look at the below version of Deploy
:
Target Deploy => _ => _
.DependsOn(Prepare)
.DependsOn(DetermineChanges)
.OnlyWhenDynamic(() => HasChanges)
.Executes(() =>
{
});
Since Nuke will build a dependency graph of all the targets before executing any of them, in this case, it will make sure that both the dependencies Deploy
and DetermineChanges
have executed before it even considers to run Deploy
. And that’s nice since it gives DetermineChanges
a chance to run that logic I mentioned and set HasChanges
to an appropriate value. Only when it is about to execute Deploy
, it will evaluate HasChanges
and decide to skip or execute the target.
There’s a caveat however. Even if HasChanges
turns out to be false
, it will still execute Prepare
and DetermineChanges
, and since Prepare
has a required parameter, it’ll still require it to contain a non-null
value. In other words, OnlyWhenDynamic
only affects the current target and not any of its dependencies. So let’s rewrite the target and use OnlyWhenStatic
instead:
Target Deploy => _ => _
.DependsOn(Prepare)
.DependsOn(DetermineChanges)
.OnlyWhenStatic(() => HasChanges)
.Executes(() =>
{
});
The big difference with the previous example, is that Nuke will now evaluate HasChanges
while it builds the dependency graph of the targets, before DetermineChanges
is executed. In other words, if HasChanges
is false
, which it is by default, neither Deploy
, nor its dependencies will be executed. And because of that, HasChanges
will never get reevaluated. As that would not be very useful, to make this work with HasChanges
, you still need to use OnlyWhenDynamic
and use it on all the targets that you want to conditionally run, including their dependencies.
But fortunately, there’s a better solution: Make the HasChanges
property do the work itself instead of relying on a target like DetermineChanges
. Then you can rely on OnlyWhenStatic
and don’t have the sprinkle your codebase with OnlyWhenDynamic
. Just make sure you cache the result of that work, for example, like this:
bool? hasChangesState = null;
bool HasChanges
{
get
{
if (!hasChangesState.HasValue)
{
hasChangesState = // execute some run-time logic to set this field
}
return hasChangesState.Value;
}
}
Target TargetUsingDynamicProperty => _ => _
.DependsOn(Prepare)
.DependsOn(DetermineChanges)
.OnlyWhenStatic(() => HasChanges)
.Executes(() =>
{
});
If only I realized that earlier…
Anyway, if you want to see this example in action, check out my demo repository.
About me
I’m a Microsoft MVP and Principal Consultant at Aviva Solutions with 28 years of experience under my belt. As a coding software architect and/or lead developer, I specialize in building or improving (legacy) full-stack enterprise solutions based on .NET as well as providing coaching on all aspects of designing, building, deploying and maintaining software systems. I’m the author of Fluent Assertions, a popular .NET assertion library, Liquid Projections, a set of libraries for building Event Sourcing projections and I’ve been maintaining coding guidelines for C# since 2001. You can find me on Twitter, Mastadon and Blue Sky.
Leave a Comment