Skip to content

feat: Add automatic .csproj/.fsproj project scanning and Bazel target generation#528

Closed
scottpledger wants to merge 1 commit intobazel-contrib:masterfrom
scottpledger:feat/csproj-support
Closed

feat: Add automatic .csproj/.fsproj project scanning and Bazel target generation#528
scottpledger wants to merge 1 commit intobazel-contrib:masterfrom
scottpledger:feat/csproj-support

Conversation

@scottpledger
Copy link

Summary

This PR introduces a major new feature that allows existing .csproj and .fsproj files to serve as the source of truth for Bazel builds. The dotnet module extension now supports a scan_projects() tag that automatically discovers .NET project files in the workspace and generates corresponding Bazel targets, enabling seamless integration with existing IDE infrastructure.

Motivation

Maintaining both .csproj/.fsproj files (for IDE support) and BUILD.bazel files (for Bazel builds) creates duplication and synchronization challenges. This feature allows developers to keep their existing .NET project files and have Bazel automatically derive build targets from them.

Features

Automatic Project Scanning

  • Scans workspace for all .csproj and .fsproj files
  • Parses project files using pure Starlark XML parsing (via xml_tools)
  • Extracts target frameworks, output types, source files, project references, and NuGet packages
  • Generates .bzl files with auto_dotnet_targets() macros in the generated dotnet_projects repository

Toolchain Validation

  • Validates that registered toolchains cover all discovered target frameworks
  • Provides suggestions for missing toolchain registrations
  • Generates TOOLCHAIN_COVERAGE.md summary in the @dotnet_projects repository (mostly for debugging)

NuGet Package Collection

  • Collects all PackageReference elements across projects
  • Resolves version conflicts using highest-version strategy
  • Generates paket.dependencies.generated to bootstrap Paket integration

IDE Support for Generated Code

  • New dotnet_generated_props rule for syncing Bazel-generated sources with IDEs
  • Uses write_source_file from bazel-lib to maintain .props files
  • Enables IntelliSense and Go-to-Definition for generated code in IDEs without Bazel support

Cross-Platform Support

  • Works on macOS, Linux, and Windows
  • Platform-specific file discovery (Unix find vs PowerShell/cmd)
  • Proper path normalization across platforms

Usage

# MODULE.bazel
dotnet = use_extension("@rules_dotnet//dotnet:extensions.bzl", "dotnet")
dotnet.toolchain(dotnet_version = "9.0.100")
dotnet.scan_projects()
use_repo(dotnet, "dotnet_toolchains", "dotnet_projects")

register_toolchains("@dotnet_toolchains//:all")
# BUILD.bazel (in directory with MyProject.csproj)
load("@dotnet_projects//path/to:MyProject.csproj.bzl", "auto_dotnet_targets")

auto_dotnet_targets(
    name = "MyProject",
    visibility = ["//visibility:public"],
)

Configuration Options

dotnet.scan_projects(
    # Exclude patterns (glob syntax)
    exclude_patterns = ["**/tests/**", "**/legacy/**"],
    
    # Fail build if TFM not covered by toolchains (default: True)
    fail_on_missing_toolchain = True,
    
    # Custom NuGet repository name
    nuget_repo_name = "dotnet_projects.nuget",
)

New Dependencies

  • xml_tools: Pure Starlark XML parser. Used for parsing .csproj/.fsproj files

Breaking Changes

None - this is an additive feature. Existing rules_dotnet usage is unaffected.

File Change Detection

Bazel automatically re-scans when:

  • Existing .csproj/.fsproj files are modified
  • New project files are added to directories already containing projects

Manual sync required (bazel sync --only=@dotnet_projects) when:

  • New project files are added to entirely new directories

Test Plan

  • Unit tests for parser (parser_test.bzl)
  • Unit tests for generator (generator_test.bzl)
  • Unit tests for NuGet collector (nuget_collector_test.bzl)
  • Unit tests for TFM utilities (tfm_utils_test.bzl)
  • E2E tests for .NET 8.0 (e2e/dotnet_projects_net8.0/)
  • E2E tests for .NET 9.0 (e2e/dotnet_projects_net9.0/)
  • E2E tests for .NET 10.0 (e2e/dotnet_projects_net10.0/)
  • All existing tests pass
  • buildifier lint passes

@scottpledger scottpledger force-pushed the feat/csproj-support branch 6 times, most recently from 56e210e to 13c6418 Compare February 1, 2026 19:07
@scottpledger scottpledger marked this pull request as ready for review February 1, 2026 19:12
@scottpledger scottpledger marked this pull request as draft February 1, 2026 19:13
@scottpledger
Copy link
Author

scottpledger commented Feb 1, 2026

This is basically ready, but I need bazelbuild/bazel-central-registry#7387 to go in, along with a follow-up release to be published so I can drop the git_overrides for xml_tools.

@scottpledger scottpledger marked this pull request as ready for review February 2, 2026 15:31
@purkhusid
Copy link
Collaborator

Thanks for taking your time to add this. I'll take a look as soon as I can, am busy with other things at the moment.
Just a quick question, is there any reason for not using Gazelle for the build file generation instead of going with this repository rule approach? Just want to get some context on why you decided on this approach.

@scottpledger
Copy link
Author

scottpledger commented Feb 4, 2026

No problem! Totally understand needing some time on this one - it's rather large.

To answer your question, I wouldn't mind using Gazelle for it, but that requires devs to make sure they run it whenever csproj/fsproj files change. My hope with this is to provide (slightly) better IDE support by allowing both IDEs and Bazel to read the csproj/fsproj directly and use them as the primary "source of truth".

I'm also considering trying to add native NuGet support now that xml.bzl exists to parse it, if that's something you'd be open to.

@scottpledger
Copy link
Author

Hey @purkhusid!

Just wanted to bump this :)

@purkhusid
Copy link
Collaborator

I've not forgotten. Sorry about the delay. I'm just swamped with various work/life things. I'll see if I can make some time this week.

@ezhao1
Copy link

ezhao1 commented Mar 9, 2026

This is awesome! I'll be excited to use this.

@purkhusid
Copy link
Collaborator

I do like the ease-of-use of these rules but at the same time I am a bit skeptical about the repository rule approach since it does mean that the extension has to be recalculated every time a project file changes and I'm not sure how that is going to perform in larger repositories.

I'm thinking that maybe we could mark these rules as experimental for a while and separate them more from the base rules if possible. This is mainly because I don't foresee me being able to help maintain this extension for the time being but at the same time I see that value in trying this out.

Could you do the following:

  • Move the code for the new extension/rules to dotnet/private/experimental
  • Is it possible to decouple the project scanning completely from the dotnet module extension into it's own module extension.

I would be comfortable with merging this if we manage to de-couple it completely form the base rules for the time being. It might even make sense at some point to just provide these rules as a separate module that could be pushed to the Bazel registry.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need e2e test for each TFM for the auto rules. How about just one e2e test with multiple projects instead?

path = "../..",
)

bazel_dep(name = "xml.bzl", version = "0.2.2")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if the base module has xml.bzl as a non-dev dependency.

path = "../..",
)

bazel_dep(name = "xml.bzl", version = "0.2.2")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if the base module has xml.bzl as a non-dev dependency.

path = "../..",
)

bazel_dep(name = "xml.bzl", version = "0.2.2")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if the base module has xml.bzl as a non-dev dependency.

path = "../..",
)

bazel_dep(name = "xml.bzl", version = "0.2.2")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if the base module has xml.bzl as a non-dev dependency.

path = "../..",
)

bazel_dep(name = "xml.bzl", version = "0.2.2")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if the base module has xml.bzl as a non-dev dependency.

path = "..",
)

bazel_dep(name = "xml.bzl", version = "0.2.2")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed if the base module has xml.bzl as a non-dev dependency.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this file. We don't commit the lockfile since we test the rules using multiple Bazel versions.

@scottpledger
Copy link
Author

It might even make sense at some point to just provide these rules as a separate module that could be pushed to the Bazel registry.

So...... Yeah, after some tweaking and revisiting my initial designs/assumptions on this, I was able to move this into its own companion module! https://github.com/scottpledger/rules_auto_dotnet contains this feature now, and should work as a partner project to rules_dotnet.

Having said that, if you ever want me to recombine these or if you'd like to help/have help with maintenance, let me know.

My company is largely a C#/web shop that has adopted Bazel for the web stuff (npm/js builds), and we're just starting to look at using it for C# builds, and I'd love to help as much as I can!

@purkhusid
Copy link
Collaborator

I think this is a good solution for the time being. I want to test out your module on my company's codebase as well to see how it works. And of course all future contributions are welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants