From 4d2e4c1238bd743561462dc4fb0bc599f9661dab Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 8 Oct 2025 11:40:52 -0700 Subject: [PATCH 01/13] Added a SpacetimeDBException to C# SDK to aid in debugging --- crates/codegen/src/csharp.rs | 4 + sdks/csharp/src/Exceptions.cs | 138 ++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 sdks/csharp/src/Exceptions.cs diff --git a/crates/codegen/src/csharp.rs b/crates/codegen/src/csharp.rs index 946eccc82af..c329f4db102 100644 --- a/crates/codegen/src/csharp.rs +++ b/crates/codegen/src/csharp.rs @@ -789,6 +789,10 @@ impl Lang for Csharp<'_> { "\"{reducer_str_name}\" => BSATNHelpers.Decode(encodedArgs)," ); } + writeln!( + output, + r#""" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"),"# + ); writeln!( output, r#"var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {{reducer}}")"# diff --git a/sdks/csharp/src/Exceptions.cs b/sdks/csharp/src/Exceptions.cs new file mode 100644 index 00000000000..c08f3a00b61 --- /dev/null +++ b/sdks/csharp/src/Exceptions.cs @@ -0,0 +1,138 @@ +using System; +using System.Runtime.Serialization; + +namespace SpacetimeDB +{ + /// + /// The base class for all SpacetimeDB SDK exceptions. + /// This allows users to catch all SpacetimeDB-specific exceptions in one catch block + /// or configure their debugger to ignore them. + /// + [Serializable] + public class SpacetimeDBException : Exception + { + // Base HRESULT for SpacetimeDB exceptions (0x8A000000 is in the user-defined range) + private const int SPACETIMEDB_HRESULT_BASE = unchecked((int)0x8A000000); + + // Specific HRESULTs for different exception types + public const int SPACETIMEDB_EMPTY_REDUCER_NAME = SPACETIMEDB_HRESULT_BASE + 1; + + public SpacetimeDBException() + : base() + { + HResult = SPACETIMEDB_HRESULT_BASE; + } + + public SpacetimeDBException(string? message) + : base(message) + { + HResult = SPACETIMEDB_HRESULT_BASE; + } + + public SpacetimeDBException(string? message, Exception? innerException) + : base(message, innerException) + { + HResult = SPACETIMEDB_HRESULT_BASE; + } + + protected SpacetimeDBException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + } + + /// + /// The exception that is thrown when one of the arguments provided to a method is not valid. + /// This is the base class for all SpacetimeDB argument exceptions. + /// + [Serializable] + public class SpacetimeDBArgumentException : SpacetimeDBException + { + private readonly string? _paramName; + + public SpacetimeDBArgumentException() + : base("Value does not fall within the expected range.") { } + + public SpacetimeDBArgumentException(string? message) + : base(message) { } + + public SpacetimeDBArgumentException(string? message, Exception? innerException) + : base(message, innerException) { } + + public SpacetimeDBArgumentException( + string? message, + string? paramName, + Exception? innerException + ) + : base(message, innerException) + { + _paramName = paramName; + } + + public SpacetimeDBArgumentException(string? message, string? paramName) + : base(message) + { + _paramName = paramName; + } + + protected SpacetimeDBArgumentException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _paramName = info.GetString("ParamName"); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("ParamName", _paramName, typeof(string)); + } + + public virtual string? ParamName => _paramName; + } + + /// + /// The exception that is thrown when an empty reducer name is received from the server. + /// This is a known condition that is handled internally by the SpacetimeDB client. + /// + [Serializable] + public class SpacetimeDBEmptyReducerNameException : SpacetimeDBArgumentException + { + public SpacetimeDBEmptyReducerNameException() + : base("Empty reducer name received from server", (string?)null) + { + HResult = SPACETIMEDB_EMPTY_REDUCER_NAME; + } + + public SpacetimeDBEmptyReducerNameException(string? paramName) + : base("Empty reducer name received from server", paramName) + { + HResult = SPACETIMEDB_EMPTY_REDUCER_NAME; + } + + public SpacetimeDBEmptyReducerNameException(string? message, string? paramName) + : base(message, paramName) + { + HResult = SPACETIMEDB_EMPTY_REDUCER_NAME; + } + + public SpacetimeDBEmptyReducerNameException(string? message, Exception? innerException) + : base(message, null, innerException) + { + HResult = SPACETIMEDB_EMPTY_REDUCER_NAME; + } + + public SpacetimeDBEmptyReducerNameException( + string? message, + string? paramName, + Exception? innerException + ) + : base(message, paramName, innerException) + { + HResult = SPACETIMEDB_EMPTY_REDUCER_NAME; + } + + protected SpacetimeDBEmptyReducerNameException( + SerializationInfo info, + StreamingContext context + ) + : base(info, context) { } + } +} From 5d01dcf75400d59b7072206d05fe751394a71bf8 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 8 Oct 2025 12:05:46 -0700 Subject: [PATCH 02/13] Update Lints --- crates/codegen/src/csharp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/codegen/src/csharp.rs b/crates/codegen/src/csharp.rs index c329f4db102..2084b0c9844 100644 --- a/crates/codegen/src/csharp.rs +++ b/crates/codegen/src/csharp.rs @@ -789,7 +789,7 @@ impl Lang for Csharp<'_> { "\"{reducer_str_name}\" => BSATNHelpers.Decode(encodedArgs)," ); } - writeln!( + writeln!( output, r#""" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"),"# ); From 70d69b7236ee0a159186430c11b7d7695fab5b24 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 8 Oct 2025 13:28:27 -0700 Subject: [PATCH 03/13] Updated codegen csharp snapshot to include new exception type --- crates/codegen/tests/snapshots/codegen__codegen_csharp.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap b/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap index 0937300d1a7..77755cee286 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap @@ -1,5 +1,6 @@ --- source: crates/codegen/tests/codegen.rs +assertion_line: 37 expression: outfiles --- "Reducers/Add.g.cs" = ''' @@ -1463,6 +1464,7 @@ namespace SpacetimeDB "say_hello" => BSATNHelpers.Decode(encodedArgs), "test" => BSATNHelpers.Decode(encodedArgs), "test_btree_index_args" => BSATNHelpers.Decode(encodedArgs), + "" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } From cd3e43363dbe3fc24b8c9a40b6b9156c83e41478 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 8 Oct 2025 13:52:01 -0700 Subject: [PATCH 04/13] Updated csharp example project module binds --- .../client/module_bindings/SpacetimeDBClient.g.cs | 1 + .../client/module_bindings/SpacetimeDBClient.g.cs | 1 + .../republishing/client/module_bindings/SpacetimeDBClient.g.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/sdks/csharp/examples~/quickstart-chat/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/quickstart-chat/client/module_bindings/SpacetimeDBClient.g.cs index 685ef94b90d..3ec6b5b9825 100644 --- a/sdks/csharp/examples~/quickstart-chat/client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/quickstart-chat/client/module_bindings/SpacetimeDBClient.g.cs @@ -472,6 +472,7 @@ protected override Reducer ToReducer(TransactionUpdate update) "ClientDisconnected" => BSATNHelpers.Decode(encodedArgs), "SendMessage" => BSATNHelpers.Decode(encodedArgs), "SetName" => BSATNHelpers.Decode(encodedArgs), + "" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs index 1da912a8d6e..7c0795d0624 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/SpacetimeDBClient.g.cs @@ -470,6 +470,7 @@ protected override Reducer ToReducer(TransactionUpdate update) "Add" => BSATNHelpers.Decode(encodedArgs), "Delete" => BSATNHelpers.Decode(encodedArgs), "ThrowError" => BSATNHelpers.Decode(encodedArgs), + "" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs index 81551e7d410..2eae003de0e 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs +++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs @@ -468,6 +468,7 @@ protected override Reducer ToReducer(TransactionUpdate update) return update.ReducerCall.ReducerName switch { "Insert" => BSATNHelpers.Decode(encodedArgs), + "" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } From 0f35f588e4f9799fc230f7e7c7d84ba40732476e Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 8 Oct 2025 14:19:37 -0700 Subject: [PATCH 05/13] Updated Blackholio's csharp bindings --- .../client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs b/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs index 2096f4ad972..8ddfb8ad3d7 100644 --- a/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs +++ b/demo/Blackholio/client-unity/Assets/Scripts/autogen/SpacetimeDBClient.g.cs @@ -491,6 +491,7 @@ protected override Reducer ToReducer(TransactionUpdate update) "spawn_food" => BSATNHelpers.Decode(encodedArgs), "suicide" => BSATNHelpers.Decode(encodedArgs), "update_player_input" => BSATNHelpers.Decode(encodedArgs), + "" => throw new SpacetimeDBEmptyReducerNameException("Reducer name is empty"), var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {reducer}") }; } From 49fb5cf6f0fc4b15d9a08b0fbc87b90e3c8e31df Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 8 Oct 2025 17:59:57 -0700 Subject: [PATCH 06/13] Adding Unity meta data file for Exceptions.cs in C# SDK --- sdks/csharp/src/Exceptions.cs.meta | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 sdks/csharp/src/Exceptions.cs.meta diff --git a/sdks/csharp/src/Exceptions.cs.meta b/sdks/csharp/src/Exceptions.cs.meta new file mode 100644 index 00000000000..fb8de1e45f6 --- /dev/null +++ b/sdks/csharp/src/Exceptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d588ff775ddb1d8449a250087d08f907 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From f2b117fd4804bc7bb3385e18bda66aa6c92962e1 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Wed, 15 Oct 2025 19:37:03 -0700 Subject: [PATCH 07/13] Update regression-test to use local ClientSDK --- .../regression-tests/republishing/client/client.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj index 3afdea6e783..759ea6f4a26 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj +++ b/sdks/csharp/examples~/regression-tests/republishing/client/client.csproj @@ -8,7 +8,7 @@ - + From a1c5445a85dc27fd4fedaa13d57ad073f036de8e Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Wed, 22 Oct 2025 22:14:06 -0700 Subject: [PATCH 08/13] [bfops/fix-csharp-quickstart-smoketest]: C# quickstart test properly uses nuget --- .github/workflows/ci.yml | 4 +-- smoketests/tests/quickstart.py | 64 +++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fae3383572..1ad49418be5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,8 @@ jobs: - uses: actions/setup-python@v5 with: { python-version: '3.12' } if: runner.os == 'Windows' - - name: Install psycopg2 - run: python -m pip install psycopg2-binary + - name: Install python deps + run: python -m pip install psycopg2-binary xmltodict - name: Run smoketests # Note: clear_database and replication only work in private run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication diff --git a/smoketests/tests/quickstart.py b/smoketests/tests/quickstart.py index 4b67578f559..edf34ffa1cd 100644 --- a/smoketests/tests/quickstart.py +++ b/smoketests/tests/quickstart.py @@ -3,6 +3,7 @@ import shutil from pathlib import Path import tempfile +import xmltodict import smoketests from .. import Smoketest, STDB_DIR, run_cmd, TEMPLATE_CARGO_TOML @@ -43,17 +44,60 @@ def _parse_quickstart(doc_path: Path, language: str) -> str: # So we could have a different db for each language return "\n".join(blocks).replace("quickstart-chat", f"quickstart-chat-{language}") + end - -def _dotnet_add_package(project_path: Path, package_name: str, source_path: Path): +def load_nuget_config(p: Path): + if p.exists(): + with p.open("rb") as f: + return xmltodict.parse(f.read(), force_list=["add", "packageSource", "package"]) + return {} + +def save_nuget_config(p: Path, doc: dict): + # Write back (pretty, UTF-8, no BOM) + xml = xmltodict.unparse(doc, pretty=True) + p.write_text(xml, encoding="utf-8") + +def add_source(doc: dict, *, key: str, path: str) -> None: + cfg = doc.setdefault("configuration", {}) + sources = cfg.setdefault("packageSources", {}) + source_entries = sources.setdefault("add", []) + source = next((s for s in source_entries if s.get("@key") == key), None) + if source is None: + source = {"@key": key, "@value": path} + source_entries.append(source) + +def add_mapping(doc: dict, *, key: str, pattern: str) -> None: + cfg = doc.setdefault("configuration", {}) + + psm = cfg.setdefault("packageSourceMapping", {}) + mapping_sources = psm.setdefault("packageSource", []) + + # Find or create the target + target = next((s for s in mapping_sources if s.get("@key") == key), None) + if target is None: + target = {"@key": key, "package": []} + mapping_sources.append(target) + + pkgs = target.setdefault("package", []) + + existing = {pkg.get("@pattern") for pkg in pkgs if "@pattern" in pkg} + if pattern not in existing: + pkgs.append({"@pattern": pattern}) + +def _dotnet_add_package(project_path: Path, package_name: str, source_path: Path, build_dir: str): """Add a local NuGet package to a .NET project""" sources = run_cmd("dotnet", "nuget", "list", "source", cwd=project_path, capture_stderr=True) - # Is the source already added? - if package_name in sources: - run_cmd("dotnet", "nuget", "remove", "source", package_name, cwd=project_path, capture_stderr=True) - run_cmd("dotnet", "nuget", "add", "source", source_path, "--name", package_name, cwd=project_path, - capture_stderr=True) + run_cmd("dotnet", "pack", cwd=source_path) + + p = Path(project_path) / "nuget.config" + doc = load_nuget_config(p) + add_source(doc, key=package_name, path=source_path/build_dir) + add_mapping(doc, key=package_name, pattern=package_name) + add_mapping(doc, key="nuget.org", pattern="*") + save_nuget_config(p, doc) + run_cmd("dotnet", "add", "package", package_name, cwd=project_path, capture_stderr=True) + run_cmd("dotnet", "nuget", "locals", "--clear", "all", cwd=project_path, capture_stderr=True) + run_cmd("dotnet", "restore", cwd=project_path, capture_stderr=True) class BaseQuickstart(Smoketest): AUTOPUBLISH = False @@ -246,11 +290,13 @@ def project_init(self, path: Path): run_cmd("dotnet", "new", "console", "--name", "QuickstartChatClient", "--output", path, capture_stderr=True) def sdk_setup(self, path: Path): - _dotnet_add_package(path, "SpacetimeDB.ClientSDK", (STDB_DIR / "sdks/csharp").absolute()) + _dotnet_add_package(STDB_DIR/"sdks/csharp", "SpacetimeDB.BSATN.Runtime", (STDB_DIR / "crates/bindings-csharp/BSATN.Runtime").absolute(), "bin/Release") + _dotnet_add_package(path, "SpacetimeDB.ClientSDK", (STDB_DIR / "sdks/csharp").absolute(), "bin~/Release") def server_postprocess(self, server_path: Path): _dotnet_add_package(server_path, "SpacetimeDB.Runtime", - (STDB_DIR / "crates/bindings-csharp/Runtime").absolute()) + (STDB_DIR / "crates/bindings-csharp/Runtime").absolute(), + "bin/Release") def test_quickstart(self): """Run the C# quickstart guides for server and client.""" From 78e6da92b2594b96be488213bb4c781df4f27a71 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 23 Oct 2025 09:18:14 -0700 Subject: [PATCH 09/13] [bfops/fix-csharp-quickstart-smoketest]: review --- smoketests/tests/quickstart.py | 72 +++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/smoketests/tests/quickstart.py b/smoketests/tests/quickstart.py index edf34ffa1cd..391cf85b87f 100644 --- a/smoketests/tests/quickstart.py +++ b/smoketests/tests/quickstart.py @@ -1,4 +1,5 @@ import logging +import os import re import shutil from pathlib import Path @@ -44,18 +45,17 @@ def _parse_quickstart(doc_path: Path, language: str) -> str: # So we could have a different db for each language return "\n".join(blocks).replace("quickstart-chat", f"quickstart-chat-{language}") + end -def load_nuget_config(p: Path): - if p.exists(): +def load_nuget_config(nuget_path: Path) -> dict: + if nuget_path.exists(): with p.open("rb") as f: return xmltodict.parse(f.read(), force_list=["add", "packageSource", "package"]) return {} -def save_nuget_config(p: Path, doc: dict): - # Write back (pretty, UTF-8, no BOM) +def save_nuget_config(nuget_path: Path, doc: dict) -> None: xml = xmltodict.unparse(doc, pretty=True) - p.write_text(xml, encoding="utf-8") + nuget_path.write_text(xml, encoding="utf-8") -def add_source(doc: dict, *, key: str, path: str) -> None: +def add_nuget_source(doc: dict, *, key: str, path: str) -> None: cfg = doc.setdefault("configuration", {}) sources = cfg.setdefault("packageSources", {}) source_entries = sources.setdefault("add", []) @@ -64,7 +64,7 @@ def add_source(doc: dict, *, key: str, path: str) -> None: source = {"@key": key, "@value": path} source_entries.append(source) -def add_mapping(doc: dict, *, key: str, pattern: str) -> None: +def add_nuget_mapping(doc: dict, *, key: str, pattern: str) -> None: cfg = doc.setdefault("configuration", {}) psm = cfg.setdefault("packageSourceMapping", {}) @@ -82,22 +82,9 @@ def add_mapping(doc: dict, *, key: str, pattern: str) -> None: if pattern not in existing: pkgs.append({"@pattern": pattern}) -def _dotnet_add_package(project_path: Path, package_name: str, source_path: Path, build_dir: str): - """Add a local NuGet package to a .NET project""" - sources = run_cmd("dotnet", "nuget", "list", "source", cwd=project_path, capture_stderr=True) - run_cmd("dotnet", "pack", cwd=source_path) - - p = Path(project_path) / "nuget.config" - doc = load_nuget_config(p) - add_source(doc, key=package_name, path=source_path/build_dir) - add_mapping(doc, key=package_name, pattern=package_name) - add_mapping(doc, key="nuget.org", pattern="*") - save_nuget_config(p, doc) - - run_cmd("dotnet", "add", "package", package_name, cwd=project_path, capture_stderr=True) - - run_cmd("dotnet", "nuget", "locals", "--clear", "all", cwd=project_path, capture_stderr=True) - run_cmd("dotnet", "restore", cwd=project_path, capture_stderr=True) +def override_nuget_package(nuget_config: dict, *, package_name: str, source_path: Path): + add_nuget_source(nuget_config, key=package_name, path=source_path.absolute()) + add_nuget_mapping(nuget_config, key=package_name, pattern=package_name) class BaseQuickstart(Smoketest): AUTOPUBLISH = False @@ -289,14 +276,43 @@ class CSharp(BaseQuickstart): def project_init(self, path: Path): run_cmd("dotnet", "new", "console", "--name", "QuickstartChatClient", "--output", path, capture_stderr=True) + nuget_config_path = Path(self.enterClassContext(tempfile.NamedTemporaryFile())) + print("NuGet config path:", nuget_config_path) + + nuget_config = load_nuget_config(nuget_config_path) + # Other packages should fall back to NuGet + add_nuget_mapping(nuget_config, key="nuget.org", pattern="*") + + # Clear any cached nuget dependencies + run_cmd("dotnet", "nuget", "locals", "--clear", "all", capture_stderr=True) + + run_cmd("dotnet", "pack", cwd=STDB_DIR/"crates/bindings-csharp") + override_nuget_package( + nuget_config, + package_name="SpacetimeDB.BSATN.Runtime", + source_path=(STDB_DIR / "crates/bindings-csharp/BSATN.Runtime/bin/Release"), + ) + override_nuget_package( + nuget_config, + package_name="SpacetimeDB.Runtime", + source_path=(STDB_DIR / "crates/bindings-csharp/Runtime/bin/Release") + ) + + run_cmd("dotnet", "pack", cwd=STDB_DIR/"sdks/csharp") + override_nuget_package( + nuget_config, + package_name="SpacetimeDB.ClientSDK", + source_path=(STDB_DIR / "sdks/csharp/bin~/Release") + ) + + save_nuget_config(nuget_config_path, nuget_config) + os.environ["NUGET_CONFIG_PATH"] = nuget_config_path + def sdk_setup(self, path: Path): - _dotnet_add_package(STDB_DIR/"sdks/csharp", "SpacetimeDB.BSATN.Runtime", (STDB_DIR / "crates/bindings-csharp/BSATN.Runtime").absolute(), "bin/Release") - _dotnet_add_package(path, "SpacetimeDB.ClientSDK", (STDB_DIR / "sdks/csharp").absolute(), "bin~/Release") + run_cmd("dotnet", "add", "package", "SpacetimeDB.ClientSDK", cwd=path, capture_stderr=True) def server_postprocess(self, server_path: Path): - _dotnet_add_package(server_path, "SpacetimeDB.Runtime", - (STDB_DIR / "crates/bindings-csharp/Runtime").absolute(), - "bin/Release") + pass def test_quickstart(self): """Run the C# quickstart guides for server and client.""" From dca33483d3ae251daedafee09716f982fa2db028 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 23 Oct 2025 10:43:58 -0700 Subject: [PATCH 10/13] [bfops/fix-csharp-quickstart-smoketest]: revert --- smoketests/tests/quickstart.py | 83 ++++++++++++++++------------------ 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/smoketests/tests/quickstart.py b/smoketests/tests/quickstart.py index 391cf85b87f..ddcef8f327e 100644 --- a/smoketests/tests/quickstart.py +++ b/smoketests/tests/quickstart.py @@ -1,5 +1,4 @@ import logging -import os import re import shutil from pathlib import Path @@ -45,26 +44,25 @@ def _parse_quickstart(doc_path: Path, language: str) -> str: # So we could have a different db for each language return "\n".join(blocks).replace("quickstart-chat", f"quickstart-chat-{language}") + end -def load_nuget_config(nuget_path: Path) -> dict: - if nuget_path.exists(): +def load_nuget_config(p: Path): + if p.exists(): with p.open("rb") as f: return xmltodict.parse(f.read(), force_list=["add", "packageSource", "package"]) return {} -def save_nuget_config(nuget_path: Path, doc: dict) -> None: +def save_nuget_config(p: Path, doc: dict): + # Write back (pretty, UTF-8, no BOM) xml = xmltodict.unparse(doc, pretty=True) - nuget_path.write_text(xml, encoding="utf-8") + p.write_text(xml, encoding="utf-8") -def add_nuget_source(doc: dict, *, key: str, path: str) -> None: +def add_source(doc: dict, *, key: str, path: str) -> None: cfg = doc.setdefault("configuration", {}) sources = cfg.setdefault("packageSources", {}) source_entries = sources.setdefault("add", []) - source = next((s for s in source_entries if s.get("@key") == key), None) - if source is None: - source = {"@key": key, "@value": path} - source_entries.append(source) + source = {"@key": key, "@value": path} + source_entries.append(source) -def add_nuget_mapping(doc: dict, *, key: str, pattern: str) -> None: +def add_mapping(doc: dict, *, key: str, pattern: str) -> None: cfg = doc.setdefault("configuration", {}) psm = cfg.setdefault("packageSourceMapping", {}) @@ -82,9 +80,21 @@ def add_nuget_mapping(doc: dict, *, key: str, pattern: str) -> None: if pattern not in existing: pkgs.append({"@pattern": pattern}) -def override_nuget_package(nuget_config: dict, *, package_name: str, source_path: Path): - add_nuget_source(nuget_config, key=package_name, path=source_path.absolute()) - add_nuget_mapping(nuget_config, key=package_name, pattern=package_name) +def override_nuget_package(project_path: Path, package_name: str, source_dir: Path, build_subdir: str): + """Override nuget config to use a local NuGet package on a .NET project""" + # Make sure the local package is built + run_cmd("dotnet", "pack", cwd=source_dir) + + p = Path(project_path) / "nuget.config" + doc = load_nuget_config(p) + add_source(doc, key=package_name, path=source_dir/build_subdir) + add_mapping(doc, key=package_name, pattern=package_name) + # Fallback for other packages + add_mapping(doc, key="nuget.org", pattern="*") + save_nuget_config(p, doc) + + # Clear any caches for nuget packages + run_cmd("dotnet", "nuget", "locals", "--clear", "all", capture_stderr=True) class BaseQuickstart(Smoketest): AUTOPUBLISH = False @@ -276,44 +286,29 @@ class CSharp(BaseQuickstart): def project_init(self, path: Path): run_cmd("dotnet", "new", "console", "--name", "QuickstartChatClient", "--output", path, capture_stderr=True) - nuget_config_path = Path(self.enterClassContext(tempfile.NamedTemporaryFile())) - print("NuGet config path:", nuget_config_path) - - nuget_config = load_nuget_config(nuget_config_path) - # Other packages should fall back to NuGet - add_nuget_mapping(nuget_config, key="nuget.org", pattern="*") - - # Clear any cached nuget dependencies - run_cmd("dotnet", "nuget", "locals", "--clear", "all", capture_stderr=True) - - run_cmd("dotnet", "pack", cwd=STDB_DIR/"crates/bindings-csharp") + def sdk_setup(self, path: Path): override_nuget_package( - nuget_config, - package_name="SpacetimeDB.BSATN.Runtime", - source_path=(STDB_DIR / "crates/bindings-csharp/BSATN.Runtime/bin/Release"), + project_dir=STDB_DIR/"sdks/csharp", + package="SpacetimeDB.BSATN.Runtime", + source_dir=(STDB_DIR / "crates/bindings-csharp/BSATN.Runtime").absolute(), + build_subdir="bin/Release" ) override_nuget_package( - nuget_config, - package_name="SpacetimeDB.Runtime", - source_path=(STDB_DIR / "crates/bindings-csharp/Runtime/bin/Release") + project_dir=path, + package="SpacetimeDB.ClientSDK", + source_dir=(STDB_DIR / "sdks/csharp").absolute(), + build_subdir="bin~/Release" ) + run_cmd("dotnet", "add", "package", "SpacetimeDB.ClientSDK", cwd=project_path, capture_stderr=True) - run_cmd("dotnet", "pack", cwd=STDB_DIR/"sdks/csharp") + def server_postprocess(self, server_path: Path): override_nuget_package( - nuget_config, - package_name="SpacetimeDB.ClientSDK", - source_path=(STDB_DIR / "sdks/csharp/bin~/Release") + project_dir=server_path, + package="SpacetimeDB.Runtime", + source_dir=(STDB_DIR / "crates/bindings-csharp/Runtime").absolute(), + build_subdir="bin/Release" ) - save_nuget_config(nuget_config_path, nuget_config) - os.environ["NUGET_CONFIG_PATH"] = nuget_config_path - - def sdk_setup(self, path: Path): - run_cmd("dotnet", "add", "package", "SpacetimeDB.ClientSDK", cwd=path, capture_stderr=True) - - def server_postprocess(self, server_path: Path): - pass - def test_quickstart(self): """Run the C# quickstart guides for server and client.""" if not smoketests.HAVE_DOTNET: From d560994a69381a9c10ae3a420109cfc63b7183e9 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 23 Oct 2025 10:49:06 -0700 Subject: [PATCH 11/13] [bfops/fix-csharp-quickstart-smoketest]: fix --- smoketests/tests/quickstart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smoketests/tests/quickstart.py b/smoketests/tests/quickstart.py index ddcef8f327e..5363a494c78 100644 --- a/smoketests/tests/quickstart.py +++ b/smoketests/tests/quickstart.py @@ -80,12 +80,12 @@ def add_mapping(doc: dict, *, key: str, pattern: str) -> None: if pattern not in existing: pkgs.append({"@pattern": pattern}) -def override_nuget_package(project_path: Path, package_name: str, source_dir: Path, build_subdir: str): +def override_nuget_package(project_dir: Path, package_name: str, source_dir: Path, build_subdir: str): """Override nuget config to use a local NuGet package on a .NET project""" # Make sure the local package is built run_cmd("dotnet", "pack", cwd=source_dir) - p = Path(project_path) / "nuget.config" + p = Path(project_dir) / "nuget.config" doc = load_nuget_config(p) add_source(doc, key=package_name, path=source_dir/build_subdir) add_mapping(doc, key=package_name, pattern=package_name) From eff3cbc195944bc82c7f432fd37b47b4d962f380 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 23 Oct 2025 11:01:43 -0700 Subject: [PATCH 12/13] [bfops/fix-csharp-quickstart-smoketest]: fix --- smoketests/tests/quickstart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smoketests/tests/quickstart.py b/smoketests/tests/quickstart.py index 5363a494c78..d66c580ee28 100644 --- a/smoketests/tests/quickstart.py +++ b/smoketests/tests/quickstart.py @@ -80,15 +80,15 @@ def add_mapping(doc: dict, *, key: str, pattern: str) -> None: if pattern not in existing: pkgs.append({"@pattern": pattern}) -def override_nuget_package(project_dir: Path, package_name: str, source_dir: Path, build_subdir: str): +def override_nuget_package(*, project_dir: Path, package: str, source_dir: Path, build_subdir: str): """Override nuget config to use a local NuGet package on a .NET project""" # Make sure the local package is built run_cmd("dotnet", "pack", cwd=source_dir) p = Path(project_dir) / "nuget.config" doc = load_nuget_config(p) - add_source(doc, key=package_name, path=source_dir/build_subdir) - add_mapping(doc, key=package_name, pattern=package_name) + add_source(doc, key=package, path=source_dir/build_subdir) + add_mapping(doc, key=package, pattern=package) # Fallback for other packages add_mapping(doc, key="nuget.org", pattern="*") save_nuget_config(p, doc) From 98cd0671947bb790da11b5bec01a89fc5293dc61 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 23 Oct 2025 11:13:28 -0700 Subject: [PATCH 13/13] [bfops/fix-csharp-quickstart-smoketest]: fix --- smoketests/tests/quickstart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoketests/tests/quickstart.py b/smoketests/tests/quickstart.py index d66c580ee28..5f2bf0ecff8 100644 --- a/smoketests/tests/quickstart.py +++ b/smoketests/tests/quickstart.py @@ -299,7 +299,7 @@ def sdk_setup(self, path: Path): source_dir=(STDB_DIR / "sdks/csharp").absolute(), build_subdir="bin~/Release" ) - run_cmd("dotnet", "add", "package", "SpacetimeDB.ClientSDK", cwd=project_path, capture_stderr=True) + run_cmd("dotnet", "add", "package", "SpacetimeDB.ClientSDK", cwd=path, capture_stderr=True) def server_postprocess(self, server_path: Path): override_nuget_package(