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 compilation
  • FileWrites – Ensures dotnet clean removes 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 Compile and FileWrites item groups to integrate with the build pipeline and dotnet clean.
  • Reference the implementation in tests/dotnet-msbuild/target-authoring/hard/CustomSdk.targets for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →