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 .

The compilation task, 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.

A [target framework moniker (TFM)][7] is a standardized token format for specifying the target framework of a .NET app or library. For example, the TFM for .NET 9 is 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-&gt;Count())' != '0'" />
    <Delete Files="$(_MvcApplicationPartAttributeGeneratedFile)" Condition="'@(_ApplicationPartAssemblyNames-&gt;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>
[Application Parts][10] allow ASP.NET Core to discover controllers, view components, tag helpers, Razor Pages e.t.c. The main use case for application parts is to configure an app to discover (or avoid loading) ASP.NET Core features from an assembly.

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