How to Create Custom MSBuild Targets with Proper Inputs/Outputs Attributes
MSBuild skips targets when every output file is newer than every input file, so you must declare both attributes to enable incremental builds and prevent unnecessary re-execution.
The dotnet/skills repository demonstrates production-ready patterns for incremental build support in custom MSBuild targets. According to the implementation in tests/dotnet-msbuild/target-authoring/hard/CustomSdk.targets, well-behaved targets declare explicit inputs and outputs, store generated files in $(IntermediateOutputPath), and register them with both Compile and FileWrites item groups.
Why Inputs and Outputs Matter for Incremental Builds
MSBuild's incremental build engine compares timestamps between the files listed in a target's Inputs and Outputs attributes. If every output file exists and is newer than every input file, the target is considered up-to-date and skipped entirely. This prevents redundant code generation and speeds up subsequent builds.
When you omit these attributes, MSBuild has no mechanism to determine freshness and executes the target on every build. The accompanying documentation in plugins/dotnet-msbuild/skills/msbuild-antipatterns/references/incremental-build-inputs-outputs.md emphasizes that both attributes are required for proper incremental build behavior.
The Three-Step Pattern for Custom Targets
Declare Inputs
The Inputs attribute specifies the files that drive the generation process. Include the project file itself using $(MSBuildProjectFile) and any source schemas or templates the target consumes.
Declare Outputs
The Outputs attribute lists the files the target creates. According to the source code, always place generated files under $(IntermediateOutputPath) (the obj folder) so they are cleaned automatically by dotnet clean.
Register Generated Files
After generating files, add them to two critical item groups:
Compile– Ensures the generated sources are included in compilationFileWrites– Ensuresdotnet cleanremoves the generated files
Complete Working Example from the dotnet/skills Repository
The CoreCodeGen target in tests/dotnet-msbuild/target-authoring/hard/CustomSdk.targets demonstrates the full pattern:
<Target Name="CoreCodeGen"
Inputs="@(CodeGenSchema)"
Outputs="@(CodeGenSchema->'$(IntermediateOutputPath)%(Filename).g.cs')">
<Message Text="Generating code from %(CodeGenSchema.Identity)" Importance="high" />
<WriteLinesToFile
File="$(IntermediateOutputPath)%(CodeGenSchema.Filename).g.cs"
Lines="// Generated from %(CodeGenSchema.Identity)"
Overwrite="true" />
<ItemGroup>
<Compile Include="@(CodeGenSchema->'$(IntermediateOutputPath)%(Filename).g.cs')" />
<FileWrites Include="@(CodeGenSchema->'$(IntermediateOutputPath)%(Filename).g.cs')" />
</ItemGroup>
</Target>
This target uses item transformation syntax (->) to map each input schema file to a corresponding output path. MSBuild tracks each input-output pair individually, allowing partial rebuilds when only specific source files change.
Additional Code Patterns
Minimal Single-File Target
For simple scenarios generating a single file:
<Target Name="GenerateBuildInfo"
Inputs="$(MSBuildProjectFile)"
Outputs="$(IntermediateOutputPath)BuildInfo.g.cs">
<WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
Lines="// Generated at $(Version)" Overwrite="true" />
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
<Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
</ItemGroup>
</Target>
Batch Processing with Item Lists
Process multiple schema files using item lists and batching:
<ItemGroup>
<CodeGenSchema Include="Schemas\*.json" />
</ItemGroup>
<Target Name="CodeGen"
Inputs="@(CodeGenSchema)"
Outputs="@(CodeGenSchema->'$(IntermediateOutputPath)%(Filename).g.cs')">
<Message Text="Generating code from %(CodeGenSchema.Identity)" />
<WriteLinesToFile
File="$(IntermediateOutputPath)%(CodeGenSchema.Filename).g.cs"
Lines="// Generated from %(CodeGenSchema.Identity)" Overwrite="true" />
<ItemGroup>
<Compile Include="@(CodeGenSchema->'$(IntermediateOutputPath)%(Filename).g.cs')" />
<FileWrites Include="@(CodeGenSchema->'$(IntermediateOutputPath)%(Filename).g.cs')" />
</ItemGroup>
</Target>
Input Validation
Add a validation target to fail early when required inputs are missing:
<Target Name="_ValidateCodeGenInputs">
<Error Text="No CodeGenSchema items defined."
Condition="'@(CodeGenSchema)' == ''" />
</Target>
Summary
- Inputs and Outputs attributes enable MSBuild's incremental build engine to skip targets when outputs are newer than inputs.
- Use
$(IntermediateOutputPath)for generated files to ensure automatic cleanup and keep the source tree clean. - Register outputs with both
CompileandFileWritesitem groups to integrate with the build pipeline anddotnet clean. - Reference the implementation in
tests/dotnet-msbuild/target-authoring/hard/CustomSdk.targetsfor production-ready patterns.
Frequently Asked Questions
What happens if I omit the Outputs attribute?
MSBuild cannot determine whether the target is up-to-date and will execute it on every build. According to the guidance in plugins/dotnet-msbuild/skills/msbuild-antipatterns/references/incremental-build-inputs-outputs.md, omitting either attribute breaks incremental builds and forces unnecessary re-execution.
Why use $(IntermediateOutputPath) for generated files?
The $(IntermediateOutputPath) property resolves to the obj folder, which is automatically excluded from source control and cleaned by dotnet clean. This keeps generated artifacts out of your source tree while ensuring they are managed by the build system.
How does MSBuild compare timestamps?
MSBuild compares the last-write time of every file listed in Inputs against every file listed in Outputs. If any input is newer than any output, or if any output is missing, the target executes. The comparison happens before the target's tasks run.
What is the FileWrites item group?
The FileWrites item group tells MSBuild which files your target creates during the build. When you run dotnet clean or msbuild /t:Clean, MSBuild deletes all files listed in this group. Without this registration, generated files persist in the obj folder across clean operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →