Most online resources will recommend using BeforeTargets
or AfterTargets
to hook your target into the MSBuild lifecycle Build
method.
<Target Name="MyPackage_BeforeBuild"
BeforeTargets="Build">
<!-- Do things -->
</Target>
<Target Name="MyPackage_AfterBuild"
AfterTargets="Build">
<!-- Do things -->
</Target>
But this breaks down if you add another target framework.
<PropertyGroup>
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<Target Name="MyPackage_BeforeBuild"
BeforeTargets="Build">
<Message Text="Before build: '$(TargetFramework)'" />
</Target>
Before build: 'net472'
Before build: 'netcoreapp3.0'
Before build: ''
Oof. Your target now runs N+1 times (if the project has N target frameworks).
At best, this slows down your build. At worst, it'll break it due to a race condition (e.g. if each iteration attempts to write to the same file).
Most of the time you can replace Build
with DispatchToInnerBuilds
.
<PropertyGroup>
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<Target Name="MyPackage_BeforeBuild"
BeforeTargets="DispatchToInnerBuilds">
<Message Text="Before build '$(TargetFramework)'" />
</Target>
Before build: ''
Success!
Well, except that DispatchToInnerBuilds
only exists for multi-targeted projects, so it will not run in a single-targeted project.
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<Target Name="MyPackage_BeforeBuild"
BeforeTargets="DispatchToInnerBuilds">
<Message Text="Before build: '$(TargetFramework)'" />
</Target>
This is the layout of a NuGet package that distributes a target that runs only once per project regardless of whether the project is single- or multi-targeted.
MyPackage.nuspec
...
<files>
<!-- files in the build/ directory run per target framework -->
<file src="MyPackage.targets" target="build" />
<!-- files in the buildMultiTargeting/ directory run once
per project (regardless of # of target frameworks),
but *only* if the project is multi-targeted -->
<file src="MyPackage.targets" target="buildMultiTargeting" />
<file src="MyPackage.props" target="buildMultiTargeting" />
</files>
Creates this package layout:
buildMultitargeting/
MyPackage.props
MyPackage.targets
build/
MyPackage.targets (same content as other MyPackage.targets)
buildMultiTargeting/MyPackage.props
<Project>
<PropertyGroup>
<!-- this file only executes in the "outer" build of a
multi-targeted project, so we set this variable to
keep track of that information -->
<IsOuterBuild>true</IsOuterBuild>
</PropertyGroup>
</Project>
build/MyPackage.targets
<PropertyGroup>
<IsOuterBuild
Condition="'$(IsOuterBuild)' == ''">false</IsOuterBuild>
<!-- Uue DispatchToInnerBuilds if a multi-targetedBuild -->
<MyBeforeTargets>BuildDependsOn</MyBeforeTargets>
<MyBeforeTargets
Condition="$(IsOuterBuild)">DispatchToInnerBuilds</MyBeforeTargets>
<MyAfterTargets>Build</MyAfterTargets>
<MyAfterTargets
Condition="$(IsOuterBuild)">DispatchToInnerBuilds</MyAfterTargets>
<!-- to prevent targets from being run extra times,
enforce that only the outer build of a multi-targeted
project or a single-targeted build can run -->
<ShouldRunTarget>false</ShouldRunTarget>
<ShouldRunTarget
Condition="'$(TargetFrameworks)' == ''
OR $(IsOuterBuild)'">true</ShouldRunTarget>
</PropertyGroup>
<Target Name="MyBeforeBuild"
Condition="$(ShouldRunTarget)"
BeforeTargets="$(MyAfterTargets)">
<!-- Do thing -->
</Target>
<Target Name="MyAfterBuild"
Condition="$(ShouldRunTarget)"
AfterTargets="$(MyAfterTargets)">
<!-- Do thing -->
</Target>
NOTE: This above snippets were edited for horizontal brevity. In production code, you should always prefix your MSBuild variables with the name of your NuGet package (e.g. ShouldRunTarget -> MyProject_ShouldRunTarget). This is because MSBuild allows overwriting of variable names, so you want to be careful not to pollute the global pool of names.