Skip to content
Open
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
fe0788e
Initial prep work
lisandroct Sep 14, 2026
1575770
Create ModuleBuilder that returns a RawModuleDef10
lisandroct Sep 14, 2026
91d2d06
Move Local and RuntimeContext to Runtime in .NET 10
lisandroct Sep 14, 2026
ec76b49
Move procedures, handlers, views and query builder to Runtime in .NET 10
lisandroct Sep 15, 2026
7a69fb0
Test that publishing a project doesn't export dependencies's entrypoints
lisandroct Sep 15, 2026
9e3dcfe
Auto discover and register dependencies
lisandroct Sep 15, 2026
4483f82
Add namespace declaration and registration
lisandroct Sep 16, 2026
0e2ab9e
Module-side ctx.Db and ctx.From now work
lisandroct Sep 16, 2026
f21aecb
Remove (maybe temporarily) the database Name and use Accessor everywhere
lisandroct Sep 16, 2026
2f6e7ab
Add SDK side
lisandroct Sep 17, 2026
ebd2cb7
Add integration tests
lisandroct Sep 17, 2026
dc13448
Scheduled reducers/procedures inside a namespace
lisandroct Sep 18, 2026
189b166
Procedure transaction writing across root and namespace tables
lisandroct Sep 18, 2026
01fba2c
Verify
lisandroct Sep 18, 2026
3d65fca
Test root-defined RLS rule targeting table in submodule
lisandroct Sep 18, 2026
d668a45
Handle env variables with the new ModuleBuilder
lisandroct Sep 21, 2026
37d7d7f
Clean GenerateQueryBuilderMembers
lisandroct Sep 21, 2026
bbf2a8e
Update namespace for dependencies
lisandroct Sep 21, 2026
f2862d8
Verify new namespaces
lisandroct Sep 21, 2026
271d4bd
Clean Runtime/Internal/Module.cs
lisandroct Sep 21, 2026
530cdc0
Increase test coverage
lisandroct Sep 21, 2026
e719ea7
Resolve namespaces only once
lisandroct Sep 21, 2026
e8afb6c
Resolve namespaces only once in module queries
lisandroct Sep 21, 2026
cd3731d
Resolve namespaces only once in clients
lisandroct Sep 21, 2026
d33dbe0
Resolve namespaces only once in reducers
lisandroct Sep 21, 2026
36ae636
Codex suggested CI change
lisandroct Sep 22, 2026
edff302
Cache index instance
lisandroct Sep 22, 2026
df13b18
Revert regression in scans
lisandroct Sep 22, 2026
0554b02
Add more checks to avoid name collisions
lisandroct Sep 23, 2026
ddfd4e7
Clean testing suite
lisandroct Sep 23, 2026
513a0f2
Improve restrictions enforcement
lisandroct Sep 23, 2026
b805c84
Remove .user file added by accident
lisandroct Sep 24, 2026
b11fc67
Remove extra test
lisandroct Sep 24, 2026
937fa1d
Simplify `SqlTableName` in `QueryBuilder`
lisandroct Sep 24, 2026
ee5e40e
Restore EnvironmentTests
lisandroct Sep 24, 2026
c5896c5
Simplify diagnostics
lisandroct Sep 24, 2026
0dba88d
Move EnvironmentGenerator.InvalidDeclaration to Diag.cs
lisandroct Sep 24, 2026
5da82f5
Run csharpier
lisandroct Sep 24, 2026
ee25591
Minor cleanup
lisandroct Sep 24, 2026
58be22e
Clean up
lisandroct Sep 24, 2026
8229065
Move ModuleDescriptorAttribute to only .NET 10
lisandroct Sep 24, 2026
5daf678
Run integration tests in both .NET 8 and .NET 10 clients
lisandroct Sep 24, 2026
3c2385a
Fix module with RLS rules being ignored
lisandroct Sep 24, 2026
6d9a511
Preserve flat accessors for dependencies mounted as "public"
lisandroct Sep 24, 2026
0609a9c
Fix public dependency overriding root's case policy
lisandroct Sep 24, 2026
8611263
Fix HTTP handlers collisions across assemblies
lisandroct Sep 24, 2026
bd7596e
Use canonical names
lisandroct Sep 25, 2026
46efdad
Fix lint error
lisandroct Sep 25, 2026
3d1b967
Fix compilation error after rebase
lisandroct Sep 30, 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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1468,9 +1468,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