Skip to content

Add .NET bindings - #475

Open
grinidx wants to merge 8 commits into
scip-code:mainfrom
dbhq-uk:dotnet-bindings
Open

Add .NET bindings#475
grinidx wants to merge 8 commits into
scip-code:mainfrom
dbhq-uk:dotnet-bindings

Conversation

@grinidx

@grinidx grinidx commented Aug 17, 2026

Copy link
Copy Markdown

Adds .NET bindings, as discussed in #468.

bindings/ had go, haskell, java, kotlin, rust and typescript, so anyone writing a .NET SCIP producer or consumer vendors scip.proto by hand today. Sourcegraph's own scip-dotnet does, and so does the tool this came out of.

The three things asked for

The bindings. Package id Scip, targeting netstandard2.0. Generated by a new protoc_builtin: csharp entry in buf.gen.yaml, alongside the existing java and kotlin ones. The generated file is committed and marked linguist-generated, matching what the other bindings do; CI regenerates and runs git diff --exit-code, so it cannot drift.

netstandard2.0 because the bindings are pure generated data types with no framework-specific surface, so it costs nothing and reaches .NET Framework 4.6.1 and above, Mono and Unity, and every modern .NET. Google.Protobuf ships a netstandard2.0 assembly.

Nix. A dotnet-bindings check in checks.nix in the same shape as go-bindings, plus what the devShell needs.

$ nix flake check
all checks passed!

$ nix build .#checks.x86_64-linux.dotnet-bindings
share/nuget/source/scip/0.9.0/scip.0.9.0.nupkg

The formatting check still passes with these files in place.

Release automation. A publish-dotnet-bindings job in release.yaml in the shape of the existing per-binding publish jobs, plus a dotnet-bindings.yaml PR check modelled on jvm-bindings.yaml, including its validation that the version matches cmd/scip/version.txt.

One wrinkle worth knowing about

protoc's C# backend emits a Descriptor class that does not compile: CS0542, because every generated protobuf type already carries a static Descriptor property. So proto-generate renames the class to SymbolDescriptor.

The message itself is untouched, and SymbolDescriptor.Descriptor.FullName still returns scip.Descriptor, so nothing about the wire format or the schema changes. It is also what sourcegraph/scip-dotnet already ships, so anyone moving between the two sees the same name.

Verified against a real index

Rather than only checking it compiles, a console app consuming the packed Scip.0.9.0.nupkg read a real scip-typescript 0.4.0 index of 60,867 bytes:

tool         scip-typescript 0.4.0
projectRoot  file:///.../ScentVerdict.Mobile
documents    4
occurrences  702
symbols      161

Re-encoding produces a byte-identical 60,867 bytes, and the reparsed index .Equals(original).

Needs something at your end

A NUGET_API_KEY Actions secret, scoped to "Push new packages and package versions". The Scip id is unregistered, so the first release creates it - worth claiming from an org account rather than a personal one if you have a preference.

Nothing else: no nix, runner or branch-protection changes.

One maintenance note for whoever does the next release: bindings/dotnet/deps.json must be regenerated whenever Google.Protobuf moves, in lock-step with flake.nix's protoc. The command is documented in checks.nix, and renovate is pinned off that dependency so it cannot bump it silently.

Adds a `protoc_builtin: csharp` entry to buf.gen.yaml alongside java and
kotlin, the generated `bindings/dotnet/src/Scip.cs`, and a project that
packs to the `Scip` NuGet package.

protoc's C# backend emits an uncompilable `Descriptor` class (CS0542), so
proto-generate renames the generated class to `SymbolDescriptor`. The
Protobuf message itself is untouched.
Builds and packs bindings/dotnet with `buildDotnetModule`, pinned to the
.NET 10 SDK, and asserts the project version matches cmd/scip/version.txt
in the same style as the Rust, Haskell and TypeScript checks.
The PR check builds bindings/dotnet with a stock SDK and validates the
package version against cmd/scip/version.txt, as the JVM bindings check
does. The release workflow packs and pushes the Scip package to NuGet
through `nix develop`, like the crate and Hackage jobs.
Adds bindings/dotnet/Scip.csproj to the list of files a version bump
touches, and a NuGet publishing section alongside the JVM one.
`Scip.Index` is ambiguous with `System.Index` under implicit usings, so the
README example aliases it. Google.Protobuf joins protobuf-java in the
renovate rule that keeps the runtime in lock-step with flake.nix's protoc.
@grinidx

grinidx commented Aug 17, 2026

Copy link
Copy Markdown
Author

One practical note on the NUGET_API_KEY, which I should have thought about when I opened this.

Scip is currently unregistered. NuGet ids are claimed on first publish and there is no way to reserve one without pushing to it, so the name stays open until whoever owns the key runs a release. Prefix reservation is a separate process and applies to prefixes rather than bare names.

The risk is low - SCIP has existed four years and nobody has taken it - but it is not zero, and this could sit for a while.

Happy to do whichever suits you:

  • You claim it. The sourcegraph account already publishes scip-dotnet and scip-excel, so that is probably a two minute job whenever you want it, and it is the obvious home.
  • We publish 0.9.0 from ours and transfer ownership to you. Only if that is genuinely more convenient, and I would rather ask than assume, because publishing first would quietly settle a decision that is yours - including whether the id should be Scip at all rather than something prefixed.

Default assumption is the first. Nothing here blocks review of the code.

@jupblb jupblb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall this looks good to me but we need to switch to Trusted Publishing. I don't want to update the API key every month (current NuGet limitation).

Please make the appropriate changes and I'll proceed with the setup on NuGet. Alternatively, I can invite you to the https://www.nuget.org/profiles/scip-code org and you can do this yourself. Take your pick.

Thank you for taking care of this, much appreciated! :)

Comment thread .github/workflows/dotnet-bindings.yaml Outdated

on:
pull_request:
paths:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm ok with this running on every PR. I prefer we didn't have to maintain this list of paths.

Comment thread flake.nix
# the descriptor pool still says `scip.Descriptor` - to
# SymbolDescriptor, the name used both by scip.proto's own
# comments and by the existing scip-dotnet bindings.
sed -i -E \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is awful and if it can't be done better with nix it's fine to leave as is. But I'll appreciate a lot if this can be replaced with something more sound and future-proof.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure this is better. 😓

Drops the `paths` filter so there is no list to keep in sync as the
project grows.
Replaces the `NUGET_API_KEY` secret with an OIDC exchange: the job
requests a GitHub id-token and `NuGet/login` trades it for an API key
that expires after an hour, so there is no long-lived key to rotate
(nuget.org now caps manually created keys at 30 days).

Needs a trust policy on nuget.org naming scip-code/scip and
release.yaml, plus a `NUGET_USER` secret holding the nuget.org profile
name that owns the policy. Both are written up in docs/Development.md.
The eight expressions stay, because the alternatives are worse: renaming
the message in a copy of scip.proto (what sourcegraph/scip-dotnet does)
drops `scip.Descriptor` from the descriptor pool, `Any` type URLs and
canonical JSON, and protoc's C# backend has no option to rename a class
without renaming the message.

What was missing was anything that fails when protoc's output moves, so
add two checks to proto-generate:

- scip.proto must still declare a message named Descriptor, so a rename
  there cannot silently reduce this step to a no-op;
- folding SymbolDescriptor back to Descriptor must reproduce protoc's
  output, which proves the expressions are a rename and nothing else.

Over-renaming now fails in proto-generate, under-renaming already failed
in the dotnet-bindings check because the file stops compiling. Both are
verified: an extra expression, and a renamed message, each abort the run.
@grinidx

grinidx commented Sep 1, 2026

Copy link
Copy Markdown
Author

All three addressed in 7b77cb5, bbe913d, 2622995.

Trusted publishing - NUGET_API_KEY is gone; the job requests an id-token and NuGet/login@v1 trades it for a one-hour key immediately before the push. The nuget.org policy details and the one NUGET_USER secret are written up in docs/Development.md.

Invite, please - happy to do the NuGet side myself, so an invite to the scip-code org would be the quicker route.

paths filter - removed; the check runs on every PR.

The sed - kept, but guarded. Renaming the message in a proto copy would change the descriptor pool, Any type URLs and canonical JSON to scip.SymbolDescriptor, which felt wrong in the protocol's own repo; and no protoc option or syntax-level tool can separate the message class from the runtime Descriptor property. So proto-generate now aborts if scip.proto stops declaring message Descriptor, and asserts that folding the rename back reproduces protoc's output byte for byte - proving it is a rename and nothing else. Both guards verified to fail loudly. If you would rather have the simpler proto-copy generation and are relaxed about the pool name, glad to switch - your call.

@jupblb jupblb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! What's your NuGet username?

Comment thread flake.nix
# the descriptor pool still says `scip.Descriptor` - to
# SymbolDescriptor, the name used both by scip.proto's own
# comments and by the existing scip-dotnet bindings.
sed -i -E \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure this is better. 😓

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.

2 participants