dotnet-affected

repository·main·Indexed 18 days ago

https://github.com/leonardochaia/dotnet-affected

A .NET tool and MSBuild SDK (DotnetAffected.Tasks) designed for large projects and mono-repositories to identify MSBuild projects affected by specific code changes. It optimizes CI/CD pipelines by analyzing project dependencies and git diffs to build and test only impacted projects. It supports outputting MSBuild Traversal SDK project files, custom commit range comparisons, and context filtering based on MSBuild properties.

Tokens
8.3K
Snippets
25
Records
30
Agent score
63%

What's inside dotnet-affected

  1. How dotnet-affected works

    main

    The tool determines which projects need to be rebuilt or retested by following these steps:

    1. Discovery: It finds all .csproj, .fsproj, and .vbproj files in the repository.
    2. Graph Building: It uses MSBuild to create a ProjectGraph representing project dependencies.
    3. Change Detection: It runs git diff to identify changed files and maps them to their respective projects. It also detects changes to Directory.Packages.props to identify NuGet package changes.
    4. Impact Analysis: It uses the ProjectGraph to find all projects transitively affected by the changed projects or packages.

    For example, if Project A is a dependency of Project B, and Project A changes, dotnet-affected will mark both Project A and Project B as affected.

  2. Use Context Filtering to filter affected projects

    main

    Context Filtering allows you to control which affected projects are actually processed based on their own evaluated MSBuild properties.

    1. Define an AffectedFilterClass: In your .props file, create an ItemGroup containing AffectedFilterClass items. Each item defines a group of properties to be retrieved from the projects.
    2. Assign Identities: Use the Include attribute to give the filter class a name (e.g., Include="No Backoffice"). This name is accessible via the %(AffectedFilterInstance.AffectedFilterClassName) metadata.
    3. Evaluate and Filter: For every impacted project, an instance of the class is created. You can then use a custom Target that runs AfterTargets="DotnetAffectedCheck" to remove projects from the ProjectReference item group based on their property values.

    Note on property behavior:

    • If a defined property does not exist in a project, the value provided in the AffectedFilterClass is used as a default.
    • For properties that DO NOT EXIST in the project, an empty value is treated as if the property exists.
    <!-- ci.props configuration -->
    <Project Sdk="DotnetAffected.Tasks;Microsoft.Build.Traversal">
        <ItemGroup>
            <AffectedFilterClass Include="No Backoffice">
                <IsBackofficeLibrary/>
            </AffectedFilterClass>
        </ItemGroup>
    
        <Target Name="_DotnetAffectedCheck" AfterTargets="DotnetAffectedCheck">
            <ItemGroup>
                <!-- Remove projects where IsBackofficeLibrary is true -->
                <ProjectReference Remove="@(AffectedFilterInstance)"
                                  Condition="'%(AffectedFilterInstance.IsBackofficeLibrary)' == 'true'"/>
            </ItemGroup>
            <Message
                Text="Role: %(AffectedFilterInstance.AffectedFilterClassName) | Filtered: %(AffectedFilterInstance.Identity)"
                Condition="'%(AffectedFilterInstance.IsBackofficeLibrary)' == 'true'" 
                Importance="high"/>
        </Target>
    </Project>
  3. Compare changes between commit ranges

    main

    By default, the tool compares your current working directory against HEAD. You can specify different comparison points using the --from and --to flags (supports branch names or commit SHAs).

    • Compare HEAD against working directory: dotnet affected
    • Compare a specific branch against HEAD: dotnet affected --from <branch-name>
    • Compare two specific branches/commits: dotnet affected --from <branch-A> --to <branch-B>
    # Compares HEAD against working directory
    dotnet affected
    
    # Compares HEAD against branch chore/target-net7
    dotnet affected --from chore/target-net7
    
    # Compares main against branch chore/target-net7
    dotnet affected --from chore/target-net7 --to main
  4. Dry run and assume changes for troubleshooting

    main

    Use these flags to observe the tool's behavior without generating actual output files.

    Dry Running

    Adding the --dry-run flag causes dotnet-affected to write its output (the content of the affected.proj file) to stdout instead of creating a file.

    $ dotnet affected --dry-run

    Assume Changes

    Use --assume-changes <project-name> to simulate changes to a specific project. This allows you to see what would be affected if that project were modified.

    $ dotnet-affected --dry-run --assume-changes dotnet-affected.Tests
  5. Build and test only affected projects using the Traversal SDK

    main

    To automate builds and tests for only the affected projects, dotnet-affected can output an MSBuild Traversal SDK project file. You can then run standard dotnet commands against this generated file.

    1. Generate the traversal project (e.g., affected.proj): dotnet affected
    2. Run tests against the generated file: dotnet test affected.proj
    dotnet affected
    dotnet test affected.proj
  6. Use dotnet-affected in Continuous Integration (CI)

    main

    For CI environments, use the --from and --to options combined with environment variables provided by your CI provider to define the range of changes.

    Building branches/tags

    To build a branch, compare the last successful build commit against the current commit hash:

    # Replace env vars with what your CI system gives you
    dotnet affected \
        --from $LAST_SUCCESSFUL_BUILD_COMMIT \
        --to $CURRENT_COMMIT_HASH
    dotnet test affected.proj

    Building Pull Requests

    To build Pull Requests, provide the target branch/commit and the PR branch/commit:

    dotnet affected generate --from origin/main --to $CURRENT_COMMIT_HASH
    dotnet test affected.proj

    Determining deployment needs

    You can determine which projects need to be redeployed by comparing the previous release tag/commit to the current one:

    dotnet affected --from releases/v1.0.0 --to releases/v2.0.0
  7. Install and use the DotnetAffected.Tasks SDK

    main

    The DotnetAffected.Tasks SDK allows you to build only the projects that have changed or projects that depend on changed projects. This is useful for optimizing enterprise-level CI builds. It uses Microsoft.Build.Traversal for execution.

    To use the SDK, follow these steps:

    1. Register the SDK: You can register it via global.json or by specifying the version directly in your project file.
    2. Create a configuration file: Create a dedicated .props file (e.g., ci.props) in the root of your git repository. This file must include both DotnetAffected.Tasks and Microsoft.Build.Traversal in its Sdk attribute.
    3. Execute builds: Run MSBuild commands (like build, test, or clean) targeting your new .props file.
    // Option 1: Register in global.json
    {
        "msbuild-sdks": {
            "DotnetAffected.Tasks": "3.0.0"
        }
    }
    <!-- Option 2: Specify version in Project file -->
    <Project Sdk="DotnetAffected.Tasks/3.0.0">
    </Project>
    <!-- Step 2: Create ci.props in git root -->
    <Project Sdk="DotnetAffected.Tasks;Microsoft.Build.Traversal">
    </Project>
    # Step 3: Run the build
    dotnet build ./ci.props
  8. Use DotnetAffected.Tasks SDK in MSBuild projects

    main

    When using an MSBuild Project SDK obtained via NuGet, you must specify a version. You can do this in two ways:

    1. Directly in the Project file: Append the version to the SDK attribute.
    2. Via global.json: Omit the version from the SDK attribute and define it in a global.json file under msbuild-sdks. This is recommended for synchronizing versions across a whole solution.

    Note: Since MSBuild 15.6, SDKs are automatically downloaded as NuGet packages.

    <!-- Option 1: Append version to package name -->
    <Project Sdk="Microsoft.Build.Traversal/2.0.12">
        ...
    </Project>
    
    <!-- Option 2: Use global.json -->
    {
        "msbuild-sdks": {
            "Microsoft.Build.Traversal": "2.0.12"
        }
    }
  9. Configure project discovery via Solution files

    main

    By default, dotnet-affected searches the --repository-path recursively for project files. If you want to limit discovery to only the projects included in a specific Solution file, use the --solution-path option.

    Note: If your Solution file is not located at the root of your Git repository, you must still provide the --repository-path to point to the .git directory.

    dotnet affected --repository-path /home/lchaia/monorepo --solution-path /home/lchaia/monorepo/my-big-project/MyBigProjectSolution.sln
  10. Install dotnet-affected

    main

    You can install dotnet-affected as a .NET tool using the dotnet tool install command. While you can install it globally using the --global flag, it is recommended to use local tools to ensure all developers and CI environments share the same version.

    dotnet tool install dotnet-affected
  11. Execute dotnet-affected directly via MSBuild (SDK-based installation)

    main

    You can execute dotnet-affected directly within an MSBuild workflow by including its tasks in a .props file. This allows you to run dotnet build on a specific props file to build only the affected projects.

    <Project Sdk="DotnetAffected.Tasks/3.0.0;Microsoft.Build.Traversal/3.2.0">
        <Target Name="_DotnetAffectedCheck" AfterTargets="DotnetAffectedCheck">
            <!-- Print all affected projects -->
            <Message Text="  >> %(ProjectReference.Identity)" Importance="high"/>
        </Target>
    </Project>

    Then run:

    dotnet build ci.props