In this article we’ll parse key, pre-compilation sections of the MSBuild generated from the project file of a Minimal API template solution in an attempt to understand how the solution is built.
We’ll use the Debug build log, at normal verbosity, as a guide to figuring out which targets in the MSBuild XML file to inspect.
This blog post assumes an introductory knowledge of MSBuild. For a quick overview of MSBuild, these articles would be helpful:
Preparation
Generating the template solution
Use the webapi
template to generate the solution. For this article, the solution is called
minimalref
.
dotnet new webapi -o minimalref
There should be both the solution file, minimalref.sln, and the main project file, minimalref.csproj in the minimalref directory.
Generating the build log
We can obtain the build log using the
-flp switch
of the dotnet msbuild
CLI tool.
dotnet msbuild minimalref.sln -flp:logfile=minimalref.log -verbosity:normal
This command saves a Debug
(default build) build log to the
minimalref.log file at the root of the project directory.
The build log should look something like this:
Build started 11/28/2024 8:37:43.
Logging verbosity is set to: Normal. 1>Project "D:\Projects\App-Workspace\Csharp\minimalref\minimalref.csproj" on node 1 (build target(s)).
1>ResolveStaticWebAssetsConfiguration:
Creating directory "obj\Debug\net8.0\staticwebassets\".
--------- other targets omitted for brevity ----------------------
1>Done Building Project "D:\Projects\App-Workspace\Csharp\minimalref\minimalref.csproj" (build target(s)).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:01.16
The full build log can be found in the build log gist .
CoreCompile
, can be found in the build log. It’s a massive task that
will only be treated briefly in this article.
Generating the MSBuild file
We’ll use the preprocess
switch of the dotnet msbuild
CLI to save the extended build to the
minimalref.xml file:
dotnet msbuild -preprocess:minimalref.xml minimalref.csproj
The expanded build should look a something like this:
<Project DefaultTargets="Build">
<PropertyGroup xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingMicrosoftNETSdkWeb>true</UsingMicrosoftNETSdkWeb>
<EnableRazorSdkContent>true</EnableRazorSdkContent>
</PropertyGroup>
<!-- omitted for brevity -->
</Project>
The full msbuild file can be found in the msbuild gist .
Inspecting the tasks
The lines in the log file indicate targets from the MSBuild that are executed (or skipped) during the build phase. We’ll walk through the targets in order, inspecting their definitions in the MSBuild file.
ResolveStaticWebAssetsConfiguration
This task creates the directory for static web assets using the
MakeDir
task. It also sets
other properties related to static assets such as the pack and build manifests.
The subset of the target shown below highlights the tasks that are run for the template minimal API project:
<Target Name="ResolveStaticWebAssetsConfiguration">
<PropertyGroup>
<!-- omitted for brevity -->
<_StaticWebAssetsIntermediateOutputPath>$(IntermediateOutputPath)staticwebassets\</_StaticWebAssetsIntermediateOutputPath>
<!-- omitted for brevity -->
</PropertyGroup>
<!-- omitted for brevity -->
<MakeDir Directories="$(_StaticWebAssetsIntermediateOutputPath)" Condition="!Exists('$(_StaticWebAssetsIntermediateOutputPath)')" />
</Target>
The IntermediateOutputPath
is based on BaseIntermediateOutputPath
and maps to the \obj
directory as seen in a <PropertyGroup>
:
<Project DefaultTargets="Build">
<!-- omitted for brevity -->
<PropertyGroup>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
<!-- omitted for brevity -->
</PropertyGroup>
<!-- omitted for brevity -->
<PropertyGroup Condition="'$(UseArtifactsOutput)' == 'true' And '$(ArtifactsPivots)' == ''" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ArtifactsPivots>$(Configuration.ToLowerInvariant())</ArtifactsPivots>
<!-- Include the TargetFramework in the pivots if the project is multi-targeted (ie TargetFrameworks) is defined -->
<ArtifactsPivots Condition="'$(TargetFrameworks)' != '' And '$(TargetFramework)' != ''">$(ArtifactsPivots)_$(TargetFramework.ToLowerInvariant())</ArtifactsPivots>
<!-- omitted for brevity -->
</PropertyGroup>
<PropertyGroup>
<!-- omitted for brevity -->
<IntermediateOutputPath Condition=" $(IntermediateOutputPath) == '' And '$(UseArtifactsIntermediateOutput)' == 'true'">$(BaseIntermediateOutputPath)$(ArtifactsPivots)\</IntermediateOutputPath>
<!-- omitted for brevity -->
</PropertyGroup>
</Project>
From the build log file, we see the task executed:
1>ResolveStaticWebAssetsConfiguration:
Creating directory "obj\Debug\net8.0\staticwebassets\".
GenerateTargetFrameworkMonikerAttribute
This target emits the target framework moniker attribute as a code fragment into a
temporary source file for the compiler using the
WriteLinesToFile
task.
net9.0
.
The target is only executed if there are changes to the output files generated by a previous compilation of the project.
<Target Name="GenerateTargetFrameworkMonikerAttribute" BeforeTargets="BeforeCompile" DependsOnTargets="PrepareForBuild;GetReferenceAssemblyPaths" Inputs="$(MSBuildToolsPath)\Microsoft.Common.targets" Outputs="$(TargetFrameworkMonikerAssemblyAttributesPath)" Condition="'@(Compile)' != '' and '$(GenerateTargetFrameworkAttribute)' == 'true'">
<WriteLinesToFile File="$(TargetFrameworkMonikerAssemblyAttributesPath)" Lines="$(TargetFrameworkMonikerAssemblyAttributeText)" Overwrite="true" ContinueOnError="true" Condition="'@(Compile)' != '' and '$(TargetFrameworkMonikerAssemblyAttributeText)' != ''" />
<ItemGroup Condition="'@(Compile)' != '' and '$(TargetFrameworkMonikerAssemblyAttributeText)' != ''">
<Compile Include="$(TargetFrameworkMonikerAssemblyAttributesPath)" />
</ItemGroup>
</Target>
This means the target doesn’t run for the first build as indicated in the build log:
GenerateTargetFrameworkMonikerAttribute:
Skipping target "GenerateTargetFrameworkMonikerAttribute" because all output files are up-to-date with respect to the input files.
CoreGenerateAssemblyInfo
This target uses the
WriteCodeFragment
task to generate the file
at the GeneratedAssemblyInfoFile
location that defines an assembly-level attribute AssemblyAttribute
and adds that to the build. The file is then added to both the Compile
and FileWrites
item
lists. Additional assembly info properties can be added to the .csproj
file; properties set in
the .csproj
will be part of the auto-generated assembly info file.
<Target Name="CoreGenerateAssemblyInfo" Condition="'$(Language)'=='VB' or '$(Language)'=='C#'" DependsOnTargets="CreateGeneratedAssemblyInfoInputsCacheFile" Inputs="$(GeneratedAssemblyInfoInputsCacheFile)" Outputs="$(GeneratedAssemblyInfoFile)" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<!-- Ensure the generated assemblyinfo file is not already part of the Compile sources, as a workaround for https://github.com/dotnet/sdk/issues/114 -->
<Compile Remove="$(GeneratedAssemblyInfoFile)" />
</ItemGroup>
<WriteCodeFragment AssemblyAttributes="@(AssemblyAttribute)" Language="$(Language)" OutputFile="$(GeneratedAssemblyInfoFile)">
<Output TaskParameter="OutputFile" ItemName="Compile" />
<Output TaskParameter="OutputFile" ItemName="FileWrites" />
</WriteCodeFragment>
</Target>
GeneratedAssemblyInfoFile
can be expanded to $(IntermediateOutputPath)$(MSBuildProjectName).
AssemblyInfo$(DefaultLanguageSourceExtension)
The
AssemblyInfo
provides properties for getting the
information about the application, such as the version number, description, loaded assemblies,
and so on. The file can be found in the intermediate directory (\obj) and looks like this:
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("minimalref")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("minimalref")]
[assembly: System.Reflection.AssemblyTitleAttribute("minimalref")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
The task is skipped if there are no changes to the input cache files. This is seen in the log of the initial build of the project:
CoreGenerateAssemblyInfo:
Skipping target "CoreGenerateAssemblyInfo" because all output files are up-to-date with respect to the input files.
_DiscoverMvcApplicationParts
This target searches for application part assemblies related to AspNetCore.Mvc, generates
attributes for them (or looks for the attribute [ApplicationPart]
)
and adds them to compilation list using a WriteCodeFragment
task:
<Target Name="_DiscoverMvcApplicationParts" Inputs="$(ProjectAssetsFile);$(MSBuildAllProjects)" Outputs="$(_MvcApplicationPartCacheFile)" DependsOnTargets="ResolveAssemblyReferences">
<ItemGroup>
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc" />
<_MvcAssemblyName Include="Microsoft.AspNetCore.Mvc.Abstractions" />
<!-- other AspNetCore.Mvc assemblies -->
</ItemGroup>
<FindAssembliesWithReferencesTo Assemblies="@(ReferencePath)" TargetAssemblyNames="@(_MvcAssemblyName)">
<Output TaskParameter="ResolvedAssemblies" ItemName="_ApplicationPartAssemblyNames" />
</FindAssembliesWithReferencesTo>
<ItemGroup>
<_MvcApplicationPartAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute">
<_Parameter1>%(_ApplicationPartAssemblyNames.Identity)</_Parameter1>
</_MvcApplicationPartAttribute>
</ItemGroup>
<WriteCodeFragment AssemblyAttributes="@(_MvcApplicationPartAttribute)" Language="$(Language)" OutputFile="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'@(_ApplicationPartAssemblyNames->Count())' != '0'" />
<Delete Files="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'@(_ApplicationPartAssemblyNames->Count())' == '0' AND Exists('$(_MvcApplicationPartAttributeGeneratedFile)')" />
<ItemGroup Condition="Exists('$(_MvcApplicationPartAttributeGeneratedFile)')">
<Compile Remove="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'!='F#'" />
<Compile Include="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'!='F#'" />
<CompileBefore Remove="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'=='F#'" />
<CompileBefore Include="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'$(Language)'=='F#'" />
<FileWrites Include="$(_MvcApplicationPartAttributeGeneratedFile)" />
</ItemGroup>
<Touch Files="$(_MvcApplicationPartCacheFile)" AlwaysCreate="true" />
<ItemGroup>
<FileWrites Include="$(_MvcApplicationPartCacheFile)" />
</ItemGroup>
</Target>
We can see from the _MvcApplicationPartAttribute
item that the target searches for an
assembly part (an application part that encapsulates an assembly reference) related to
Identity
. This assembly is not found if you do not configure the minimal web api with
Identity. We see this in the log file:
If the generated attribute file is found but no assembly names discovered, the generated file
is deleted as indicated by the
Delete
task.
If no application parts were found, an empty cache file is generated that is not added to the
compilation list as indicated by the
Touch
task and the FileWrites
item group. We can
see this in the build log for the minimal API template:
_DiscoverMvcApplicationParts:
Could not infer the type of parameter "#1" because the attribute type is unknown. The value will be treated as a string.
Could not infer the type of parameter "#1" because the attribute type is unknown. The value will be treated as a string.
Creating "obj\Debug\net8.0\minimalref.MvcApplicationPartsAssemblyInfo.cache" because "AlwaysCreate" was specified.
Touching "obj\Debug\net8.0\minimalref.MvcApplicationPartsAssemblyInfo.cache".
_GenerateSourceLinkFile
This target uses the task
GenerateSourceLinkFile
to build a source link for the project.
<UsingTask TaskName="Microsoft.SourceLink.Common.GenerateSourceLinkFile" AssemblyFile="$(_MicrosoftSourceLinkCommonAssemblyFile)" />
<Target Name="_SetSourceLinkFilePath">
<PropertyGroup>
<_SourceLinkFilePath>$(IntermediateOutputPath)$(MSBuildProjectName).sourcelink.json</_SourceLinkFilePath>
</PropertyGroup>
</Target>
SourceLink
][15]
is a technology that enables source code debugging of .NET assemblies from NuGet by devs. Source Link executes when creating the NuGet package and embeds source control metadata inside assemblies and the package. You can open a NuGet package in Rider’s
Assembly Explorer
If a source link is not configured for the project via a PropertyGroup
, the target is skipped:
_GenerateSourceLinkFile:
Source Link is empty, file 'obj\Debug\net8.0\minimalref.sourcelink.json' does not exist.
Compilation target
The compilation target is quite huge and will be the subject of a future article. It’s listed in this article as a form of closing.
CoreCompile
This task compiles the project using the CLI command dotnet.exe exec
with a list of arguments.
<Target name="CoreCompile">
<!-- omitted for brevity -->
</Target>
Summary
In this article, we covered the pre-compilation build targets. In the next article, we’ll cover the post-compilation targets.
The next part in the series is here