diff --git a/crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs b/crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs index 9b6aec79abe..eeba596929b 100644 --- a/crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs +++ b/crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs @@ -203,6 +203,12 @@ public void Write(BinaryWriter writer, Inner? value) public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => AlgebraicType.MakeOption(innerRW.GetAlgebraicType(registrar)); + + // Return a List BSATN serializer that can serialize this option as an array + public static List GetListSerializer() + { + return new List(); + } } // This implementation is nearly identical to RefOption. The only difference is the constraint on T. diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs index 6c3e5217fa5..f48ca103131 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/diag/snapshots/Module#FFI.verified.cs @@ -1,4 +1,4 @@ -//HintName: FFI.cs +//HintName: FFI.cs // #nullable enable // The runtime already defines SpacetimeDB.Internal.LocalReadOnly in Runtime\Internal\Module.cs as an empty partial type. @@ -1959,6 +1959,9 @@ public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx public static List ToListOrEmpty(T? value) where T : struct => value is null ? new List() : new List { value.Value }; + public static List ToListOrEmpty(T? value) + where T : class => value is null ? new List() : new List { value }; + #if EXPERIMENTAL_WASM_AOT // In AOT mode we're building a library. // Main method won't be called automatically, so we need to export it as a preinit function. diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs index 00c082e7ef7..09a465916e5 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#FFI.verified.cs @@ -1,4 +1,4 @@ -//HintName: FFI.cs +//HintName: FFI.cs // #nullable enable // The runtime already defines SpacetimeDB.Internal.LocalReadOnly in Runtime\Internal\Module.cs as an empty partial type. @@ -1699,6 +1699,9 @@ public void Invoke(BinaryReader reader, SpacetimeDB.Internal.IReducerContext ctx public static List ToListOrEmpty(T? value) where T : struct => value is null ? new List() : new List { value.Value }; + public static List ToListOrEmpty(T? value) + where T : class => value is null ? new List() : new List { value }; + #if EXPERIMENTAL_WASM_AOT // In AOT mode we're building a library. // Main method won't be called automatically, so we need to export it as a preinit function. diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 25e065e2ec6..ee0d690bb5b 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -978,6 +978,7 @@ public ViewDeclaration(GeneratorAttributeSyntaxContext context, DiagReporter dia // Validate return type: must be Option or Vec if ( !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption") + && !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.RefOption") && !ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.List") ) { @@ -1030,8 +1031,10 @@ public string GenerateDispatcherClass(uint index) ? "SpacetimeDB.AnonymousViewContext" : "SpacetimeDB.ViewContext"; - var isValueOption = ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption"); - var writeOutput = isValueOption + var isOption = + ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.ValueOption") + || ReturnType.BSATNName.Contains("SpacetimeDB.BSATN.RefOption"); + var writeOutput = isOption ? $$$""" var listSerializer = {{{ReturnType.BSATNName}}}.GetListSerializer(); var listValue = ModuleRegistration.ToListOrEmpty(returnValue); @@ -1934,6 +1937,9 @@ static class ModuleRegistration { public static List ToListOrEmpty(T? value) where T : struct => value is null ? new List() : new List { value.Value }; + public static List ToListOrEmpty(T? value) where T : class + => value is null ? new List() : new List { value }; + #if EXPERIMENTAL_WASM_AOT // In AOT mode we're building a library. // Main method won't be called automatically, so we need to export it as a preinit function. diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs index 7871e616b03..1c646c9d78b 100644 --- a/sdks/csharp/examples~/regression-tests/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/client/Program.cs @@ -63,6 +63,8 @@ void OnConnected(DbConnection conn, Identity identity, string authToken) .Subscribe([ "SELECT * FROM example_data", "SELECT * FROM my_player", + "SELECT * FROM my_account", + "SELECT * FROM my_account_missing", "SELECT * FROM players_at_level_one", "SELECT * FROM my_table", "SELECT * FROM null_string_nonnullable", @@ -296,11 +298,21 @@ void OnSubscriptionApplied(SubscriptionEventContext context) // Views test Log.Debug("Checking Views are populated"); Debug.Assert(context.Db.MyPlayer != null, "context.Db.MyPlayer != null"); + Debug.Assert(context.Db.MyAccount != null, "context.Db.MyAccount != null"); + Debug.Assert(context.Db.MyAccountMissing != null, "context.Db.MyAccountMissing != null"); Debug.Assert(context.Db.PlayersAtLevelOne != null, "context.Db.PlayersAtLevelOne != null"); Debug.Assert( context.Db.MyPlayer.Count > 0, $"context.Db.MyPlayer.Count = {context.Db.MyPlayer.Count}" ); + Debug.Assert( + context.Db.MyAccount.Count == 1, + $"context.Db.MyAccount.Count = {context.Db.MyAccount.Count}" + ); + Debug.Assert( + context.Db.MyAccountMissing.Count == 0, + $"context.Db.MyAccountMissing.Count = {context.Db.MyAccountMissing.Count}" + ); Debug.Assert( context.Db.PlayersAtLevelOne.Count > 0, $"context.Db.PlayersAtLevelOne.Count = {context.Db.PlayersAtLevelOne.Count}" 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 e114efeef41..7a6f628f1dc 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 @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.11.1 (commit b00ba57ed047514aca886b89d52b44520a7ac43d). +// This was generated using spacetimedb cli version 1.11.1 (commit 8dd18f078fed83a2a3946e7bc037b17f103b8026). #nullable enable @@ -30,7 +30,10 @@ public RemoteTables(DbConnection conn) { AddTable(Admins = new(conn)); AddTable(User = new(conn)); + AddTable(Account = new(conn)); AddTable(ExampleData = new(conn)); + AddTable(MyAccount = new(conn)); + AddTable(MyAccountMissing = new(conn)); AddTable(MyLog = new(conn)); AddTable(MyPlayer = new(conn)); AddTable(MyTable = new(conn)); diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Account.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Account.g.cs new file mode 100644 index 00000000000..6c6e6e38fb9 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/Account.g.cs @@ -0,0 +1,49 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + public sealed partial class RemoteTables + { + public sealed class AccountHandle : RemoteTableHandle + { + protected override string RemoteTableName => "account"; + + public sealed class IdUniqueIndex : UniqueIndexBase + { + protected override ulong GetKey(Account row) => row.Id; + + public IdUniqueIndex(AccountHandle table) : base(table) { } + } + + public readonly IdUniqueIndex Id; + + public sealed class IdentityUniqueIndex : UniqueIndexBase + { + protected override SpacetimeDB.Identity GetKey(Account row) => row.Identity; + + public IdentityUniqueIndex(AccountHandle table) : base(table) { } + } + + public readonly IdentityUniqueIndex Identity; + + internal AccountHandle(DbConnection conn) : base(conn) + { + Id = new(this); + Identity = new(this); + } + + protected override object GetPrimaryKey(Account row) => row.Id; + } + + public readonly AccountHandle Account; + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/MyAccount.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/MyAccount.g.cs new file mode 100644 index 00000000000..c442807347a --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/MyAccount.g.cs @@ -0,0 +1,27 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + public sealed partial class RemoteTables + { + public sealed class MyAccountHandle : RemoteTableHandle + { + protected override string RemoteTableName => "my_account"; + + internal MyAccountHandle(DbConnection conn) : base(conn) + { + } + } + + public readonly MyAccountHandle MyAccount; + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/MyAccountMissing.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/MyAccountMissing.g.cs new file mode 100644 index 00000000000..71870aa5811 --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Tables/MyAccountMissing.g.cs @@ -0,0 +1,27 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + public sealed partial class RemoteTables + { + public sealed class MyAccountMissingHandle : RemoteTableHandle + { + protected override string RemoteTableName => "my_account_missing"; + + internal MyAccountMissingHandle(DbConnection conn) : base(conn) + { + } + } + + public readonly MyAccountMissingHandle MyAccountMissing; + } +} diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/Account.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/Account.g.cs new file mode 100644 index 00000000000..f3418a9aefe --- /dev/null +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Types/Account.g.cs @@ -0,0 +1,39 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Types +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class Account + { + [DataMember(Name = "Id")] + public ulong Id; + [DataMember(Name = "Identity")] + public SpacetimeDB.Identity Identity; + [DataMember(Name = "Name")] + public string Name; + + public Account( + ulong Id, + SpacetimeDB.Identity Identity, + string Name + ) + { + this.Id = Id; + this.Identity = Identity; + this.Name = Name; + } + + public Account() + { + this.Name = ""; + } + } +} diff --git a/sdks/csharp/examples~/regression-tests/server/Lib.cs b/sdks/csharp/examples~/regression-tests/server/Lib.cs index 454220eecbd..5adc5d46a30 100644 --- a/sdks/csharp/examples~/regression-tests/server/Lib.cs +++ b/sdks/csharp/examples~/regression-tests/server/Lib.cs @@ -73,6 +73,19 @@ public partial struct Player public string Name; } + [SpacetimeDB.Table(Name = "account", Public = true)] + public partial class Account + { + [SpacetimeDB.PrimaryKey] + [SpacetimeDB.AutoInc] + public ulong Id; + + [SpacetimeDB.Unique] + public Identity Identity; + + public string Name = ""; + } + [SpacetimeDB.Table(Name = "player_level", Public = true)] public partial struct PlayerLevel { @@ -137,7 +150,19 @@ public partial struct NullStringNullable [SpacetimeDB.View(Name = "my_player", Public = true)] public static Player? MyPlayer(ViewContext ctx) { - return ctx.Db.player.Identity.Find(ctx.Sender) as Player?; + return ctx.Db.player.Identity.Find(ctx.Sender); + } + + [SpacetimeDB.View(Name = "my_account", Public = true)] + public static Account? MyAccount(ViewContext ctx) + { + return ctx.Db.account.Identity.Find(ctx.Sender) as Account; + } + + [SpacetimeDB.View(Name = "my_account_missing", Public = true)] + public static Account? MyAccountMissing(ViewContext ctx) + { + return null; } // Multiple rows: return a list @@ -269,6 +294,11 @@ public static void ClientConnected(ReducerContext ctx) ctx.Db.player_level.Insert(new PlayerLevel { PlayerId = playerId, Level = 1 }); } + if (ctx.Db.account.Identity.Find(ctx.Sender) is null) + { + ctx.Db.account.Insert(new Account { Identity = ctx.Sender, Name = "Account" }); + } + if (ctx.Db.nullable_vec.Id.Find(1) is null) { ctx.Db.nullable_vec.Insert(new NullableVec