Claude Cursor GitHub Copilot Skill

directory-build-organization

Guide for organizing MSBuild infrastructure with Directory.Build.props, Directory.Build.targets, Directory.Packages.props, and Directory.Build.rsp. USE FOR: structuring multi-project repos, centralizing build settings, implementing NuGet Central Package Management (CPM) with Mana

LLM Mart · 0 points · 13 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dotnet-skills-plugins_dotnet-msbuild_skills_directory-build-organization-98f8485.zip · 6 KB
Part of dotnet/skills — 119 skills

Install

skills CLI npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-msbuild/skills/directory-build-organization
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
Git git clone https://github.com/dotnet/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Organizing Build Infrastructure with Directory.Build Files

Directory.Build.props vs Directory.Build.targets

Understanding which file to use is critical. They differ in when they are imported during evaluation:

Evaluation order:

Directory.Build.props → SDK .props → YourProject.csproj → SDK .targets → Directory.Build.targets
Use .props for Use .targets for
Setting property defaults Custom build targets
Common item definitions Late-bound property overrides
Properties projects can override Post-build steps
Assembly/package metadata Conditional logic on final values
Analyzer PackageReferences Targets that depend on SDK-defined properties

Rule of thumb: Properties and items go in .props. Custom targets and late-bound logic go in .targets.

Because .props is imported before the project file, the project can override any value set there. Because .targets is imported after everything, it gets the final say—but projects cannot override .targets values.

⚠️ Critical: TargetFramework Availability in .props vs .targets

Property conditions on $(TargetFramework) in .props files silently fail for single-targeting projects — the property is empty during .props evaluation. Move TFM-conditional properties to .targets instead. ItemGroup and Target conditions are not affected.

See targetframework-props-pitfall.md for the full explanation.

Directory.Build.props

Good candidates: language settings, assembly/package metadata, build warnings, code analysis, common analyzers.

<Project>
  <PropertyGroup>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <Company>Contoso</Company>
    <Authors>Contoso Engineering</Authors>
  </PropertyGroup>
</Project>

Do NOT put here: project-specific TFMs, project-specific PackageReferences, targets/build logic, or properties depending on SDK-defined values (not available during .props evaluation).

Directory.Build.targets

Good candidates: custom build targets, late-bound property overrides (values depending on SDK properties), post-build validation.

<Project>
  <Target Name="ValidateProjectSettings" BeforeTargets="Build">
    <Error Text="All libraries must target netstandard2.0 or higher"
           Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
  </Target>

  <PropertyGroup>
    <!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
    <DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
  </PropertyGroup>
</Project>

Directory.Packages.props (Central Package Management)

Central Package Management (CPM) provides a single source of truth for all NuGet package versions. See https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management for details.

Enable CPM in Directory.Packages.props at the repo root:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
    <PackageVersion Include="xunit" Version="2.9.0" />
    <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
  </ItemGroup>

  <ItemGroup>
    <!-- GlobalPackageReference applies to ALL projects — great for analyzers -->
    <GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
    <GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
  </ItemGroup>
</Project>

Directory.Build.rsp

Contains default MSBuild CLI arguments applied to all builds under the directory tree.

Example Directory.Build.rsp:

/maxcpucount
/nodeReuse:false
/consoleLoggerParameters:Summary;ForceNoAlign
/warnAsMessage:MSB3277
  • Works with both msbuild and dotnet CLI in modern .NET versions
  • Great for enforcing consistent CI and local build flags
  • Each argument goes on its own line

Multi-level Directory.Build Files

MSBuild only auto-imports the first Directory.Build.props (or .targets) it finds walking up from the project directory. To chain multiple levels, explicitly import the parent at the top of the inner file. See multi-level-examples for full file examples.

<Project>
  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
         Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />

  <!-- Inner-level overrides go here -->
</Project>

Example layout:

repo/
  Directory.Build.props          ← repo-wide (lang version, company info, analyzers)
  Directory.Build.targets        ← repo-wide targets
  Directory.Packages.props       ← central package versions
  src/
    Directory.Build.props        ← src-specific (imports repo-level, sets IsPackable=true)
  test/
    Directory.Build.props        ← test-specific (imports repo-level, sets IsPackable=false, adds test packages)

Artifact Output Layout (.NET 8+)

Set <ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath> in Directory.Build.props to automatically produce project-name-separated bin/, obj/, and publish/ directories under a single artifacts/ folder, avoiding bin/obj clashes by default. See common-patterns for the directory layout and additional patterns (conditional settings by project type, post-pack validation).

Workflow: Organizing Build Infrastructure

  1. Audit all .csproj files — Catalog every <PropertyGroup>, <ItemGroup>, and custom <Target> across the solution. Note which settings repeat and which are project-specific.
  2. Create root Directory.Build.props — Move shared property defaults (LangVersion, Nullable, TreatWarningsAsErrors, metadata) here. These are imported before the project file so projects can override them.
  3. Create root Directory.Build.targets — Move custom build targets, post-build validation, and any properties that depend on SDK-defined values (e.g., OutputPath, TargetFramework for single-targeting projects) here. These are imported after the SDK so all properties are available.
  4. Create Directory.Packages.props — Enable Central Package Management (ManagePackageVersionsCentrally), list all PackageVersion entries, and remove Version= from PackageReference items in .csproj files.
  5. Set up multi-level hierarchy — Create inner Directory.Build.props files for src/ and test/ folders with distinct settings. Use GetPathOfFileAbove to chain to the parent.
  6. Simplify .csproj files — Remove all centralized properties, version attributes, and duplicated targets. Each project should only contain what is unique to it.
  7. Validate — Run dotnet restore && dotnet build and verify no regressions. Use dotnet msbuild -pp:output.xml to inspect the final merged view if needed.

Troubleshooting

Problem Cause Fix
Directory.Build.props isn't picked up File name casing wrong (exact match required on Linux/macOS) Verify exact casing: Directory.Build.props (capital D, B)
Properties from .props are ignored by projects Project sets the same property after the import Move the property to Directory.Build.targets to set it after the project
Multi-level import doesn't work Missing GetPathOfFileAbove import in inner file Add the <Import> element at the top of the inner file (see Multi-level section)
Properties using SDK values are empty in .props SDK properties aren't defined yet during .props evaluation Move to .targets which is imported after the SDK
Directory.Packages.props not found File not at repo root or not named exactly Must be named Directory.Packages.props and at or above the project directory
Property condition on $(TargetFramework) doesn't match in .props TargetFramework isn't set yet for single-targeting projects during .props evaluation Move property to .targets, or use ItemGroup/Target conditions instead (which evaluate late)

Diagnosis: Use the preprocessed project output to see all imports and final property values:

dotnet msbuild -pp:output.xml MyProject.csproj

This expands all imports inline so you can see exactly where each property is set and what the final evaluated value is.

Files (skills)
  • references
    • common-patterns.md 1.4 KB
      # Common Directory.Build Patterns
      
      ## Conditional Settings by Project Type
      
      Detect test projects by naming convention in `Directory.Build.props`:
      
      ```xml
      <PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.UnitTests'))">
        <IsPackable>false</IsPackable>
        <IsTestProject>true</IsTestProject>
      </PropertyGroup>
      ```
      
      Use `Directory.Build.targets` for conditions on SDK-defined properties like `OutputType`:
      
      ```xml
      <PropertyGroup Condition="'$(OutputType)' == 'Exe'">
        <SelfContained>false</SelfContained>
      </PropertyGroup>
      
      <PropertyGroup Condition="'$(OutputType)' == 'Library' AND '$(IsTestProject)' != 'true'">
        <GenerateDocumentationFile>true</GenerateDocumentationFile>
      </PropertyGroup>
      ```
      
      ## Post-Build Validation
      
      Validate that `Pack` produced the expected output:
      
      ```xml
      <Target Name="ValidatePackageOutput" AfterTargets="Pack"
              Condition="'$(IsPackable)' == 'true'">
        <Error Text="Package was not created at $(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg"
               Condition="!Exists('$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg')" />
      </Target>
      ```
      
      ## Artifact Output Layout (.NET 8+)
      
      Setting `ArtifactsPath` in `Directory.Build.props` produces this structure:
      
      ```
      artifacts/
        bin/
          MyLib/
            debug/
            release/
          MyApp/
            debug/
            release/
        obj/
          MyLib/
          MyApp/
        publish/
          MyApp/
      ```
      
    • multi-level-examples.md 3.9 KB
      # Multi-level Directory.Build Examples
      
      Full file examples for a typical multi-level repo layout.
      
      ## Repo-level `Directory.Build.props`
      
      ```xml
      <Project>
      
        <PropertyGroup>
          <Nullable>enable</Nullable>
          <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        </PropertyGroup>
      
      </Project>
      ```
      
      ## `src/Directory.Build.props`
      
      ```xml
      <Project>
      
        <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
               Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
      
        <PropertyGroup>
          <IsPackable>true</IsPackable>
          <GenerateDocumentationFile>true</GenerateDocumentationFile>
        </PropertyGroup>
      
      </Project>
      ```
      
      ## `test/Directory.Build.props`
      
      ```xml
      <Project>
      
        <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
               Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
      
        <PropertyGroup>
          <IsPackable>false</IsPackable>
          <NoWarn>$(NoWarn);CS1591</NoWarn>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageReference Include="xunit" />
          <PackageReference Include="xunit.runner.visualstudio" />
          <PackageReference Include="Microsoft.NET.Test.Sdk" />
          <PackageReference Include="NSubstitute" />
        </ItemGroup>
      
      </Project>
      ```
      
      ## Before/After: Centralizing Duplicated Settings
      
      **Before — duplicated settings in every .csproj:**
      
      ```xml
      <!-- src/LibA/LibA.csproj -->
      <Project Sdk="Microsoft.NET.Sdk">
      
        <PropertyGroup>
          <TargetFramework>net8.0</TargetFramework>
          <Nullable>enable</Nullable>
          <ImplicitUsings>enable</ImplicitUsings>
          <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
          <Company>Contoso</Company>
          <Authors>Contoso Engineering</Authors>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
          <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
        </ItemGroup>
      
      </Project>
      
      <!-- src/LibB/LibB.csproj — same boilerplate repeated -->
      <Project Sdk="Microsoft.NET.Sdk">
      
        <PropertyGroup>
          <TargetFramework>net8.0</TargetFramework>
          <Nullable>enable</Nullable>
          <ImplicitUsings>enable</ImplicitUsings>
          <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
          <Company>Contoso</Company>
          <Authors>Contoso Engineering</Authors>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
          <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
        </ItemGroup>
      
      </Project>
      ```
      
      **After — centralized with Directory.Build files:**
      
      ```xml
      <!-- Directory.Build.props -->
      <Project>
      
        <PropertyGroup>
          <Nullable>enable</Nullable>
          <ImplicitUsings>enable</ImplicitUsings>
          <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
          <Company>Contoso</Company>
          <Authors>Contoso Engineering</Authors>
        </PropertyGroup>
      
      </Project>
      
      <!-- Directory.Packages.props -->
      <Project>
      
        <PropertyGroup>
          <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
          <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
        </ItemGroup>
      
        <ItemGroup>
          <GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
        </ItemGroup>
      
      </Project>
      
      <!-- src/LibA/LibA.csproj — clean and minimal -->
      <Project Sdk="Microsoft.NET.Sdk">
      
        <PropertyGroup>
          <TargetFramework>net8.0</TargetFramework>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageReference Include="Newtonsoft.Json" />
        </ItemGroup>
      
      </Project>
      
      <!-- src/LibB/LibB.csproj — clean and minimal -->
      <Project Sdk="Microsoft.NET.Sdk">
      
        <PropertyGroup>
          <TargetFramework>net8.0</TargetFramework>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageReference Include="Microsoft.Extensions.Logging" />
        </ItemGroup>
      
      </Project>
      ```
      
    • targetframework-props-pitfall.md 3.3 KB
      # AP-21: Property Conditioned on TargetFramework in .props Files
      
      **Smell**: `<PropertyGroup Condition="'$(TargetFramework)' == '...'">` or `<Property Condition="'$(TargetFramework)' == '...'">` in `Directory.Build.props` or any `.props` file imported before the project body.
      
      **Why it's bad**: `$(TargetFramework)` is NOT reliably available in `Directory.Build.props` or any `.props` file imported before the project body. It is only set that early for multi-targeting projects, which receive `TargetFramework` as a global property from the outer build. Single-targeting projects (using singular `<TargetFramework>`) set it in the project body, which is evaluated *after* `.props`. This means property conditions on `$(TargetFramework)` in `.props` files silently fail for single-targeting projects — the condition never matches because the property is empty. This applies to both `<PropertyGroup Condition="...">` and individual `<Property Condition="...">` elements.
      
      For a detailed explanation of MSBuild's evaluation and execution phases, see [Build process overview](https://learn.microsoft.com/en-us/visualstudio/msbuild/build-process-overview).
      
      ```xml
      <!-- BAD: In Directory.Build.props — TargetFramework may be empty here -->
      <PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
        <DefineConstants>$(DefineConstants);MY_FEATURE</DefineConstants>
      </PropertyGroup>
      
      <!-- ALSO BAD: Condition on the property itself has the same problem -->
      <PropertyGroup>
        <DefineConstants Condition="'$(TargetFramework)' == 'net8.0'">$(DefineConstants);MY_FEATURE</DefineConstants>
      </PropertyGroup>
      
      <!-- GOOD: In Directory.Build.targets — TargetFramework is always available -->
      <PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
        <DefineConstants>$(DefineConstants);MY_FEATURE</DefineConstants>
      </PropertyGroup>
      
      <!-- ALSO GOOD: In the project file itself -->
      <!-- MyProject.csproj -->
      <PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
        <DefineConstants>$(DefineConstants);MY_FEATURE</DefineConstants>
      </PropertyGroup>
      ```
      
      **⚠️ Item and Target conditions are NOT affected.** This restriction applies ONLY to property conditions (`<PropertyGroup Condition="...">` and `<Property Condition="...">`). Item conditions (`<ItemGroup Condition="...">`) and Target conditions in `.props` files are SAFE because items and targets evaluate after all properties (including those set in the project body) have been evaluated. This includes `PackageVersion` items in `Directory.Packages.props`, `PackageReference` items in `Directory.Build.props`, and any other item types.
      
      **Do NOT flag the following patterns — they are correct:**
      
      ```xml
      <!-- OK in Directory.Build.props — ItemGroup conditions evaluate late -->
      <ItemGroup Condition="'$(TargetFramework)' == 'net472'">
        <PackageReference Include="System.Memory" />
      </ItemGroup>
      
      <!-- OK in Directory.Packages.props — PackageVersion items evaluate late -->
      <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
        <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
      </ItemGroup>
      <ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
        <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
      </ItemGroup>
      
      <!-- OK — Individual item conditions also evaluate late -->
      <ItemGroup>
        <PackageReference Include="System.Memory" Condition="'$(TargetFramework)' == 'net472'" />
      </ItemGroup>
      ```
      
  • SKILL.md 9.6 KB
    ---
    name: directory-build-organization
    description: "Guide for organizing MSBuild infrastructure with Directory.Build.props, Directory.Build.targets, Directory.Packages.props, and Directory.Build.rsp. USE FOR: structuring multi-project repos, centralizing build settings, implementing NuGet Central Package Management (CPM) with ManagePackageVersionsCentrally, consolidating duplicated properties across .csproj files, setting up multi-level Directory.Build hierarchy with GetPathOfFileAbove, understanding evaluation order (Directory.Build.props → SDK .props → .csproj → SDK .targets → Directory.Build.targets). Critical pitfall: $(TargetFramework) conditions in .props silently fail for single-targeting projects — must use .targets. DO NOT USE FOR: non-MSBuild build systems, migrating legacy projects to SDK-style (use msbuild-modernization), single-project solutions with no shared settings."
    license: MIT
    ---
    
    # Organizing Build Infrastructure with Directory.Build Files
    
    ## Directory.Build.props vs Directory.Build.targets
    
    Understanding which file to use is critical. They differ in **when** they are imported during evaluation:
    
    **Evaluation order:**
    
    ```
    Directory.Build.props → SDK .props → YourProject.csproj → SDK .targets → Directory.Build.targets
    ```
    
    | Use `.props` for | Use `.targets` for |
    |---|---|
    | Setting property defaults | Custom build targets |
    | Common item definitions | Late-bound property overrides |
    | Properties projects can override | Post-build steps |
    | Assembly/package metadata | Conditional logic on final values |
    | Analyzer PackageReferences | Targets that depend on SDK-defined properties |
    
    **Rule of thumb:** Properties and items go in `.props`. Custom targets and late-bound logic go in `.targets`.
    
    Because `.props` is imported before the project file, the project can override any value set there. Because `.targets` is imported after everything, it gets the final say—but projects cannot override `.targets` values.
    
    ### ⚠️ Critical: TargetFramework Availability in .props vs .targets
    
    **Property conditions on `$(TargetFramework)` in `.props` files silently fail for single-targeting projects** — the property is empty during `.props` evaluation. Move TFM-conditional properties to `.targets` instead. ItemGroup and Target conditions are not affected.
    
    See [targetframework-props-pitfall.md](references/targetframework-props-pitfall.md) for the full explanation.
    
    ## Directory.Build.props
    
    Good candidates: language settings, assembly/package metadata, build warnings, code analysis, common analyzers.
    
    ```xml
    <Project>
      <PropertyGroup>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
        <Company>Contoso</Company>
        <Authors>Contoso Engineering</Authors>
      </PropertyGroup>
    </Project>
    ```
    
    **Do NOT put here:** project-specific TFMs, project-specific PackageReferences, targets/build logic, or properties depending on SDK-defined values (not available during `.props` evaluation).
    
    ## Directory.Build.targets
    
    Good candidates: custom build targets, late-bound property overrides (values depending on SDK properties), post-build validation.
    
    ```xml
    <Project>
      <Target Name="ValidateProjectSettings" BeforeTargets="Build">
        <Error Text="All libraries must target netstandard2.0 or higher"
               Condition="'$(OutputType)' == 'Library' AND '$(TargetFramework)' == 'net472'" />
      </Target>
    
      <PropertyGroup>
        <!-- DocumentationFile depends on OutputPath, which is set by the SDK -->
        <DocumentationFile Condition="'$(IsPackable)' == 'true'">$(OutputPath)$(AssemblyName).xml</DocumentationFile>
      </PropertyGroup>
    </Project>
    ```
    
    ## Directory.Packages.props (Central Package Management)
    
    Central Package Management (CPM) provides a single source of truth for all NuGet package versions. See [https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) for details.
    
    **Enable CPM in `Directory.Packages.props` at the repo root:**
    
    ```xml
    <Project>
      <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
        <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
        <PackageVersion Include="xunit" Version="2.9.0" />
        <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
      </ItemGroup>
    
      <ItemGroup>
        <!-- GlobalPackageReference applies to ALL projects — great for analyzers -->
        <GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
        <GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
      </ItemGroup>
    </Project>
    ```
    
    ## Directory.Build.rsp
    
    Contains default MSBuild CLI arguments applied to all builds under the directory tree.
    
    **Example `Directory.Build.rsp`:**
    
    ```
    /maxcpucount
    /nodeReuse:false
    /consoleLoggerParameters:Summary;ForceNoAlign
    /warnAsMessage:MSB3277
    ```
    
    - Works with both `msbuild` and `dotnet` CLI in modern .NET versions
    - Great for enforcing consistent CI and local build flags
    - Each argument goes on its own line
    
    ## Multi-level Directory.Build Files
    
    MSBuild only auto-imports the **first** `Directory.Build.props` (or `.targets`) it finds walking up from the project directory. To chain multiple levels, explicitly import the parent at the **top** of the inner file. See [multi-level-examples](references/multi-level-examples.md) for full file examples.
    
    ```xml
    <Project>
      <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
             Condition="Exists('$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))')" />
    
      <!-- Inner-level overrides go here -->
    </Project>
    ```
    
    **Example layout:**
    
    ```
    repo/
      Directory.Build.props          ← repo-wide (lang version, company info, analyzers)
      Directory.Build.targets        ← repo-wide targets
      Directory.Packages.props       ← central package versions
      src/
        Directory.Build.props        ← src-specific (imports repo-level, sets IsPackable=true)
      test/
        Directory.Build.props        ← test-specific (imports repo-level, sets IsPackable=false, adds test packages)
    ```
    
    ## Artifact Output Layout (.NET 8+)
    
    Set `<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>` in `Directory.Build.props` to automatically produce project-name-separated `bin/`, `obj/`, and `publish/` directories under a single `artifacts/` folder, avoiding bin/obj clashes by default. See [common-patterns](references/common-patterns.md) for the directory layout and additional patterns (conditional settings by project type, post-pack validation).
    
    ## Workflow: Organizing Build Infrastructure
    
    1. **Audit all `.csproj` files** — Catalog every `<PropertyGroup>`, `<ItemGroup>`, and custom `<Target>` across the solution. Note which settings repeat and which are project-specific.
    2. **Create root `Directory.Build.props`** — Move shared property defaults (LangVersion, Nullable, TreatWarningsAsErrors, metadata) here. These are imported before the project file so projects can override them.
    3. **Create root `Directory.Build.targets`** — Move custom build targets, post-build validation, and any properties that depend on SDK-defined values (e.g., `OutputPath`, `TargetFramework` for single-targeting projects) here. These are imported after the SDK so all properties are available.
    4. **Create `Directory.Packages.props`** — Enable Central Package Management (`ManagePackageVersionsCentrally`), list all `PackageVersion` entries, and remove `Version=` from `PackageReference` items in `.csproj` files.
    5. **Set up multi-level hierarchy** — Create inner `Directory.Build.props` files for `src/` and `test/` folders with distinct settings. Use `GetPathOfFileAbove` to chain to the parent.
    6. **Simplify `.csproj` files** — Remove all centralized properties, version attributes, and duplicated targets. Each project should only contain what is unique to it.
    7. **Validate** — Run `dotnet restore && dotnet build` and verify no regressions. Use `dotnet msbuild -pp:output.xml` to inspect the final merged view if needed.
    
    ## Troubleshooting
    
    | Problem | Cause | Fix |
    |---|---|---|
    | `Directory.Build.props` isn't picked up | File name casing wrong (exact match required on Linux/macOS) | Verify exact casing: `Directory.Build.props` (capital D, B) |
    | Properties from `.props` are ignored by projects | Project sets the same property after the import | Move the property to `Directory.Build.targets` to set it after the project |
    | Multi-level import doesn't work | Missing `GetPathOfFileAbove` import in inner file | Add the `<Import>` element at the top of the inner file (see Multi-level section) |
    | Properties using SDK values are empty in `.props` | SDK properties aren't defined yet during `.props` evaluation | Move to `.targets` which is imported after the SDK |
    | `Directory.Packages.props` not found | File not at repo root or not named exactly | Must be named `Directory.Packages.props` and at or above the project directory |
    | Property condition on `$(TargetFramework)` doesn't match in `.props` | `TargetFramework` isn't set yet for single-targeting projects during `.props` evaluation | Move property to `.targets`, or use ItemGroup/Target conditions instead (which evaluate late) |
    
    **Diagnosis:** Use the preprocessed project output to see all imports and final property values:
    
    ```bash
    dotnet msbuild -pp:output.xml MyProject.csproj
    ```
    
    This expands all imports inline so you can see exactly where each property is set and what the final evaluated value is.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related