Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
586cd9f
Initial prep work
lisandroct Sep 14, 2026
eee125c
Create ModuleBuilder that returns a RawModuleDef10
lisandroct Sep 14, 2026
8bf4267
Move Local and RuntimeContext to Runtime in .NET 10
lisandroct Sep 14, 2026
f76db94
Move procedures, handlers, views and query builder to Runtime in .NET 10
lisandroct Sep 15, 2026
496771b
Test that publishing a project doesn't export dependencies's entrypoints
lisandroct Sep 15, 2026
aaef1d3
Auto discover and register dependencies
lisandroct Sep 15, 2026
14a54f0
Add namespace declaration and registration
lisandroct Sep 16, 2026
c6e27a0
Module-side ctx.Db and ctx.From now work
lisandroct Sep 16, 2026
a61fe51
Remove (maybe temporarily) the database Name and use Accessor everywhere
lisandroct Sep 16, 2026
08c33fb
Add SDK side
lisandroct Sep 17, 2026
e3d0412
Add integration tests
lisandroct Sep 17, 2026
d18ecdd
Scheduled reducers/procedures inside a namespace
lisandroct Sep 18, 2026
cf7e23a
Procedure transaction writing across root and namespace tables
lisandroct Sep 18, 2026
8a44580
Verify
lisandroct Sep 18, 2026
ce10ad1
Test root-defined RLS rule targeting table in submodule
lisandroct Sep 18, 2026
28585a0
Handle env variables with the new ModuleBuilder
lisandroct Sep 21, 2026
24c35c0
Clean GenerateQueryBuilderMembers
lisandroct Sep 21, 2026
cbb91aa
Update namespace for dependencies
lisandroct Sep 21, 2026
3c80bfb
Verify new namespaces
lisandroct Sep 21, 2026
c9e40c6
Clean Runtime/Internal/Module.cs
lisandroct Sep 21, 2026
9c85479
Increase test coverage
lisandroct Sep 21, 2026
1c754ae
Resolve namespaces only once
lisandroct Sep 21, 2026
3f7c582
Resolve namespaces only once in module queries
lisandroct Sep 21, 2026
6eb91a5
Resolve namespaces only once in clients
lisandroct Sep 21, 2026
8d8fb3e
Resolve namespaces only once in reducers
lisandroct Sep 21, 2026
80e0c20
Codex suggested CI change
lisandroct Sep 22, 2026
8e66423
Cache index instance
lisandroct Sep 22, 2026
56f7626
Revert regression in scans
lisandroct Sep 22, 2026
33feb8e
Add more checks to avoid name collisions
lisandroct Sep 23, 2026
c65452d
Clean testing suite
lisandroct Sep 23, 2026
8600a63
Improve restrictions enforcement
lisandroct Sep 23, 2026
bb4d909
Remove .user file added by accident
lisandroct Sep 24, 2026
cc32028
Remove extra test
lisandroct Sep 24, 2026
ef8984c
Simplify `SqlTableName` in `QueryBuilder`
lisandroct Sep 24, 2026
2eed5ce
Restore EnvironmentTests
lisandroct Sep 24, 2026
7bc237f
Simplify diagnostics
lisandroct Sep 24, 2026
f1f7bd2
Move EnvironmentGenerator.InvalidDeclaration to Diag.cs
lisandroct Sep 24, 2026
5666a45
Run csharpier
lisandroct Sep 24, 2026
d108e01
Minor cleanup
lisandroct Sep 24, 2026
6bde0c7
Clean up
lisandroct Sep 24, 2026
894390c
Move ModuleDescriptorAttribute to only .NET 10
lisandroct Sep 24, 2026
e7f6c99
Run integration tests in both .NET 8 and .NET 10 clients
lisandroct Sep 24, 2026
b5028b6
Fix module with RLS rules being ignored
lisandroct Sep 24, 2026
9977afe
Preserve flat accessors for dependencies mounted as "public"
lisandroct Sep 24, 2026
afe7798
Fix public dependency overriding root's case policy
lisandroct Sep 24, 2026
2025539
Fix HTTP handlers collisions across assemblies
lisandroct Sep 24, 2026
fa97ddc
Use canonical names
lisandroct Sep 25, 2026
fd4afcd
Fix lint error
lisandroct Sep 25, 2026
7ee3c50
Fix csharp-testsuite
lisandroct Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1410,7 +1410,7 @@ jobs:
# Create temp global.json to target .NET 8 SDK for workload install
echo '{"sdk":{"version":"8.0.100","rollForward":"latestFeature"}}' > global.json
dotnet workload install wasi-experimental
rm global.json
git restore -- global.json

- name: Override NuGet packages
run: |
Expand All @@ -1430,9 +1430,9 @@ jobs:
working-directory: sdks/csharp
run: dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln

- name: Run .NET tests
- name: Run C# SDK tests
working-directory: sdks/csharp
run: dotnet test -warnaserror --no-restore SpacetimeDB.ClientSDK.csproj
run: dotnet test -warnaserror --no-restore tests~/tests.csproj

- name: Verify C# formatting
working-directory: sdks/csharp
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 29 additions & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,34 @@ of either the module libraries or client SDKs:
5. Repeat steps 2 through 4 for any other client projects which ought to cover the same behavior.
6. Run the new test with the relevant `cargo test` command for that SDK.

## C# namespace coverage

The namespace tests are integration tests, plus focused generator diagnostics:

- `dotnet test crates/bindings-csharp/Codegen.Tests -f net8.0` and
`-f net10.0`: existing Verify fixtures on both targets; .NET 10 additionally
checks mount restrictions, generated-name diagnostics, and dependency discovery.
- `cargo test -p spacetimedb-testing --test standalone_integration_test namespace_csharp`:
independent dependency publication, root export selection, and cross-namespace
helper/HTTP calls, and runtime name conversion compared with host validation.
- `cargo test -p spacetimedb-testing --test environment namespace_csharp_environment_security`:
root/public environment access, denial of namespaced host calls, and default
namespace case conversion when `Name` is omitted.
- `cargo test -p spacetimedb-codegen --test codegen`: codegen snapshots and a
generated C# client for a TypeScript submodule, including distinct accessor
and canonical names.
- The [namespace client regression](sdks/csharp/examples~/regression-tests/namespaces/README.md)
runs against the real .NET 10 module and covers tables, queries, subscriptions,
callbacks, scheduling, transactions, and root-defined RLS with
`Accessor = "MyAuth", Name = "auth_data"`. The existing
`sdks/csharp/tools~/run-regression-tests.sh 8 10` harness includes it in the
.NET 10 module pass while retaining .NET 8 regressions.

Use the local package setup in [DEVELOP.md](sdks/csharp/DEVELOP.md), not stale
published NuGet packages, when exercising changed query/runtime code.
Library-defined RLS inside named namespaces and cross-language module composition
are unsupported, not integration cases awaiting a passing assertion.

## Schema parity tests

`crates/schema/tests/ensure_same_schema.rs` is a separate but important companion to the SDK tests.
Expand Down Expand Up @@ -142,4 +170,4 @@ To add a new module library to the Standalone integration test suite:
and so you are free to ignore them.
3. Modify `crates/testing/tests/standalone_integration_test.rs` to define new `#[test] #[serial]` test functions
which use your new `module-test-XX` module to do the same operations as the existing tests.
4. Run the tests with `cargo test -p spacetimedb-testing --test standalone_integration_test`.
4. Run the tests with `cargo test -p spacetimedb-testing --test standalone_integration_test`.
2 changes: 1 addition & 1 deletion crates/bindings-csharp/BSATN.Codegen/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ INamedTypeSymbol type
typeSyntax
.Members.OfType<FieldDeclarationSyntax>()
.SelectMany(f => f.Declaration.Variables)
.Select(v => type.GetMembers(v.Identifier.Text).OfType<IFieldSymbol>().Single())
.Select(v => type.GetMembers(v.Identifier.ValueText).OfType<IFieldSymbol>().Single())
.Where(f => !f.IsStatic);

public static IFieldSymbol? FindSpacetimeDbField(ITypeSymbol rowType, string fieldName)
Expand Down
6 changes: 6 additions & 0 deletions crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ public interface IStructuralReadWrite
/// <typeparam name="T"></typeparam>
/// <param name="reader"></param>
/// <returns></returns>
#if NET10_0_OR_GREATER
// Avoid an extra per-row call in NativeAOT scan iterators.
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining
)]
#endif
static T Read<T>(BinaryReader reader)
where T : IStructuralReadWrite, new()
{
Expand Down
49 changes: 40 additions & 9 deletions crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@ namespace SpacetimeDB;
using System;
using System.Globalization;

/// <summary>A table identifier with a separately quoted optional namespace and local name.</summary>
public readonly struct SqlTableName
{
public string? Namespace { get; }
public string LocalName { get; }

public SqlTableName(string localName)
{
LocalName = localName;
Namespace = null;
}

public SqlTableName(string @namespace, string localName)
{
Namespace = @namespace ?? throw new ArgumentNullException(nameof(@namespace));
LocalName = localName;
}

public override string ToString() =>
Namespace is null
? SqlFormat.QuoteIdent(LocalName)
: SqlFormat.QuoteIdent(Namespace) + "." + SqlFormat.QuoteIdent(LocalName);
}

public readonly struct SqlLiteral<T>
{
internal string Sql { get; }
Expand Down Expand Up @@ -113,11 +137,13 @@ internal IxJoinEq(string leftRefSql, string rightRefSql)
}
}

public readonly struct Col<TRow, TValue>(string tableName, string columnName)
public readonly struct Col<TRow, TValue>(SqlTableName tableName, string columnName)
where TValue : notnull
{
internal string RefSql =>
$"{SqlFormat.QuoteIdent(tableName)}.{SqlFormat.QuoteIdent(columnName)}";
public Col(string tableName, string columnName)
: this(new SqlTableName(tableName), columnName) { }

internal string RefSql => $"{tableName}.{SqlFormat.QuoteIdent(columnName)}";

public BoolExpr<TRow> Eq(SqlLiteral<TValue> value) => new($"({RefSql} = {value.Sql})");

Expand Down Expand Up @@ -146,11 +172,13 @@ public readonly struct Col<TRow, TValue>(string tableName, string columnName)
public override string ToString() => RefSql;
}

public readonly struct IxCol<TRow, TValue>(string tableName, string columnName)
public readonly struct IxCol<TRow, TValue>(SqlTableName tableName, string columnName)
where TValue : notnull
{
internal string RefSql =>
$"{SqlFormat.QuoteIdent(tableName)}.{SqlFormat.QuoteIdent(columnName)}";
public IxCol(string tableName, string columnName)
: this(new SqlTableName(tableName), columnName) { }

internal string RefSql => $"{tableName}.{SqlFormat.QuoteIdent(columnName)}";

public BoolExpr<TRow> Eq(SqlLiteral<TValue> value) => new($"({RefSql} = {value.Sql})");

Expand All @@ -162,16 +190,19 @@ public IxJoinEq<TRow, TOtherRow> Eq<TOtherRow>(IxCol<TOtherRow, TValue> other) =
public override string ToString() => RefSql;
}

public sealed class Table<TRow, TCols, TIxCols>(string tableName, TCols cols, TIxCols ixCols)
public sealed class Table<TRow, TCols, TIxCols>(SqlTableName tableName, TCols cols, TIxCols ixCols)
: IQuery<TRow>
{
internal string TableRefSql => SqlFormat.QuoteIdent(tableName);
public Table(string tableName, TCols cols, TIxCols ixCols)
: this(new SqlTableName(tableName), cols, ixCols) { }

internal string TableRefSql => tableName.ToString();

internal TCols Cols => cols;

internal TIxCols IxCols => ixCols;

public string ToSql() => $"SELECT * FROM {SqlFormat.QuoteIdent(tableName)}";
public string ToSql() => $"SELECT * FROM {TableRefSql}";

public FromWhere<TRow, TCols, TIxCols> Where<TPredicate>(Func<TCols, TPredicate> predicate) =>
new(this, QueryPredicate.ToBoolExpr<TRow>(predicate(cols)!));
Expand Down
9 changes: 5 additions & 4 deletions crates/bindings-csharp/Codegen.Tests/Codegen.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CSharpier.Core" Version="0.28.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Verify.SourceGenerators" Version="2.4.3" />
<PackageReference Include="Verify.XUnit" Version="26.4.5" />
Expand All @@ -26,6 +25,7 @@
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net8.'))">
<PackageReference Include="CSharpier.Core" Version="1.1.2" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
Expand All @@ -35,9 +35,10 @@
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net10.'))">
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" />
<PackageReference Include="CSharpier.Core" Version="1.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion crates/bindings-csharp/Codegen.Tests/EnvironmentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public static void InvalidDeclarationsAreCompileErrors(string field)
Assert.Contains(
result.Diagnostics,
diagnostic =>
diagnostic.Id == "STDBENV001" && diagnostic.Severity == DiagnosticSeverity.Error
diagnostic.Id == "STDB0039" && diagnostic.Severity == DiagnosticSeverity.Error
);
}

Expand Down
35 changes: 28 additions & 7 deletions crates/bindings-csharp/Codegen.Tests/TestInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,34 @@ namespace SpacetimeDB.Codegen.Tests;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

// Global Verify setup for all tests we might have.
static class TestInit
{
internal static (string Code, IEnumerable<Diagnostic> Errors) FormatCode(string code)
{
var header = "";
// CSharpier reorders conditional global imports below ordinary imports.
// Keep this already-formatted header intact and format the declarations only.
if (code.Contains("global using "))
{
var root = CSharpSyntaxTree.ParseText(code).GetCompilationUnitRoot();
var headerEnd = root.Usings.Last().FullSpan.End;
header = code[..headerEnd];
code = code[headerEnd..];
}
var result = CSharpier.Core.CSharp.CSharpFormatter.Format(
code,
new() { IncludeGenerated = true, EndOfLine = CSharpier.Core.EndOfLine.LF }
);
#if NET10_0_OR_GREATER
return (header + result.Code, result.ErrorDiagnostics);
#else
return (header + result.Code, result.CompilationErrors);
#endif
}

// A custom Diagnostic converter that pretty-prints the error with the source code snippet and squiggly underline.
// TODO: upstream this?
class DiagConverter : WriteOnlyJsonConverter<Diagnostic>
Expand All @@ -31,7 +55,7 @@ public override void Write(VerifyJsonWriter writer, Diagnostic diag)
{
var line = lines[lineIdx];
// print the source line
comment.AppendLine(line.ToString());
comment.AppendLine(line.ToString().TrimEnd());
// print squiggly line highlighting the location
if (line.Span.Intersection(loc.SourceSpan) is { } intersection)
{
Expand Down Expand Up @@ -73,17 +97,14 @@ public static void Initialize()
{
var unformattedCode = sb.ToString();
sb.Clear();
var result = CSharpier.CodeFormatter.Format(
unformattedCode,
new() { IncludeGenerated = true, EndOfLine = CSharpier.EndOfLine.LF }
);
var result = FormatCode(unformattedCode);
sb.Append(result.Code);
// Print errors in the end so that their line numbers are still meaningful.
if (result.CompilationErrors.Any())
if (result.Errors.Any())
{
sb.AppendLine();
sb.AppendLine("// Generated code produced compilation errors:");
foreach (var diag in result.CompilationErrors)
foreach (var diag in result.Errors)
{
sb.Append("// ").AppendLine(diag.ToString());
}
Expand Down
Loading
Loading