Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
77e3c29
Initial prep work
lisandroct Sep 14, 2026
1948715
Create ModuleBuilder that returns a RawModuleDef10
lisandroct Sep 14, 2026
ba04049
Move Local and RuntimeContext to Runtime in .NET 10
lisandroct Sep 14, 2026
bd623b8
Move procedures, handlers, views and query builder to Runtime in .NET 10
lisandroct Sep 15, 2026
efe88fb
Test that publishing a project doesn't export dependencies's entrypoints
lisandroct Sep 15, 2026
c4e8b30
Auto discover and register dependencies
lisandroct Sep 15, 2026
63d40f4
Add namespace declaration and registration
lisandroct Sep 16, 2026
2a66649
Module-side ctx.Db and ctx.From now work
lisandroct Sep 16, 2026
4bd0afb
Remove (maybe temporarily) the database Name and use Accessor everywhere
lisandroct Sep 16, 2026
59b737c
Add SDK side
lisandroct Sep 17, 2026
acf1adc
Add integration tests
lisandroct Sep 17, 2026
40e17ee
Scheduled reducers/procedures inside a namespace
lisandroct Sep 18, 2026
174d7f7
Procedure transaction writing across root and namespace tables
lisandroct Sep 18, 2026
926b517
Verify
lisandroct Sep 18, 2026
98d305e
Test root-defined RLS rule targeting table in submodule
lisandroct Sep 18, 2026
ded1b17
Handle env variables with the new ModuleBuilder
lisandroct Sep 21, 2026
f9c4cbf
Clean GenerateQueryBuilderMembers
lisandroct Sep 21, 2026
ec18507
Update namespace for dependencies
lisandroct Sep 21, 2026
6c4061a
Verify new namespaces
lisandroct Sep 21, 2026
ca0931b
Clean Runtime/Internal/Module.cs
lisandroct Sep 21, 2026
a0d52de
Increase test coverage
lisandroct Sep 21, 2026
c51e6d2
Resolve namespaces only once
lisandroct Sep 21, 2026
171ac91
Resolve namespaces only once in module queries
lisandroct Sep 21, 2026
7a44fc9
Resolve namespaces only once in clients
lisandroct Sep 21, 2026
8d0b0b2
Resolve namespaces only once in reducers
lisandroct Sep 21, 2026
1873c12
Codex suggested CI change
lisandroct Sep 22, 2026
4ceb43e
Cache index instance
lisandroct Sep 22, 2026
8cc54a7
Revert regression in scans
lisandroct Sep 22, 2026
36ea347
Add more checks to avoid name collisions
lisandroct Sep 23, 2026
3308c83
Clean testing suite
lisandroct Sep 23, 2026
0c1a562
Improve restrictions enforcement
lisandroct Sep 23, 2026
4d21966
Remove .user file added by accident
lisandroct Sep 24, 2026
b00398d
Remove extra test
lisandroct Sep 24, 2026
8610966
Simplify `SqlTableName` in `QueryBuilder`
lisandroct Sep 24, 2026
8091ec5
Restore EnvironmentTests
lisandroct Sep 24, 2026
592fa67
Simplify diagnostics
lisandroct Sep 24, 2026
b5a15e9
Move EnvironmentGenerator.InvalidDeclaration to Diag.cs
lisandroct Sep 24, 2026
e88f3c3
Run csharpier
lisandroct Sep 24, 2026
d1d0232
Minor cleanup
lisandroct Sep 24, 2026
504bf6a
Clean up
lisandroct Sep 24, 2026
9ee0be9
Move ModuleDescriptorAttribute to only .NET 10
lisandroct Sep 24, 2026
6ad9ecb
Run integration tests in both .NET 8 and .NET 10 clients
lisandroct Sep 24, 2026
3673350
Fix module with RLS rules being ignored
lisandroct Sep 24, 2026
4993793
Preserve flat accessors for dependencies mounted as "public"
lisandroct Sep 24, 2026
9027f84
Fix public dependency overriding root's case policy
lisandroct Sep 24, 2026
7060366
Fix HTTP handlers collisions across assemblies
lisandroct Sep 24, 2026
79a68d0
Use canonical names
lisandroct Sep 25, 2026
82df8ce
Fix lint error
lisandroct Sep 25, 2026
ed9a65e
Fix compilation error after rebase
lisandroct Sep 30, 2026
0b42613
Ignore HTTP routes in submodules
lisandroct Oct 6, 2026
18e2b47
Fix CI
lisandroct Oct 9, 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 @@ -2131,9 +2131,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