Skip to content

Commit 4989ba9

Browse files
authored
Rekhoff/csharp default field values (#3235)
# Description of Changes This is the implementation of issue #3191. This adds a Default attribute to C# module fields. **Note**: In C#, attribute arguments must be compile-time constants, which means you can't directly use non-constant expressions like new expressions, method calls, or dynamic values in attribute constructors. (Ref: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/attributes#2324-attribute-parameter-types) For this reason, these default values are limited to primitive types, enums, and strings. This includes (shown as `C# Type` (`BSATN type`): * `bool` (`Bool`) * `sbyte` (`I8`) * `byte` (`U8`) * `short` (`I16`) * `ushort` (`U16`) * `int` (`I32`) * `unit` (`U32`) * `long` (`I64`) * `ulong` (`U64`) * `float` (`F32`) * `double` (`F64`) * `enum` (`Enum`) * `string` (`String`) * `null` (`RefOption`) <- Nullable type Because of C# limitations, for nullable and complex data types, such as a struct, can take use `[Default(null)]` to populate values with null defaults. This allows things like structs to workaround the non-constant expressions in attribute constructors limitation by allowing these complex types to still be able to be added as new column tables. The `int` type can also be in the form of Hex or Binary literals, such as `[Default(0x2A)]` or `[Default(0b00101010)]` Both Decimal (like `[Default(3.14m)]`) and Char (like `[Default('A')]`) are unsupported types in BSATN and will still return `BSATN0001` errors. # API and ABI breaking changes Not API breaking. This change only adds the `[Default(value)]` attribute logic. Using the `[Default(value)]` attribute with older versions SpacetimeDB C# modules will result in an error. # Expected complexity level and risk 2 # Testing Local testing of this requires use of CLI changes in #3278 - [x] Regression test of functionality added.
1 parent 50e8183 commit 4989ba9

File tree

18 files changed

+1063
-1
lines changed

18 files changed

+1063
-1
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/// Regression tests run with a live server.
2+
/// To run these, run a local SpacetimeDB via `spacetime start`,
3+
/// then in a separate terminal run `tools~/run-regression-tests.sh PATH_TO_SPACETIMEDB_REPO_CHECKOUT`.
4+
/// This is done on CI in .github/workflows/test.yml.
5+
6+
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
8+
using SpacetimeDB;
9+
using SpacetimeDB.Types;
10+
11+
const string HOST = "http://localhost:3000";
12+
const string DBNAME = "republish-test";
13+
14+
DbConnection ConnectToDB()
15+
{
16+
DbConnection? conn = null;
17+
conn = DbConnection.Builder()
18+
.WithUri(HOST)
19+
.WithModuleName(DBNAME)
20+
.OnConnect(OnConnected)
21+
.OnConnectError((err) =>
22+
{
23+
throw err;
24+
})
25+
.OnDisconnect((conn, err) =>
26+
{
27+
if (err != null)
28+
{
29+
throw err;
30+
}
31+
else
32+
{
33+
throw new Exception("Unexpected disconnect");
34+
}
35+
})
36+
.Build();
37+
return conn;
38+
}
39+
40+
uint waiting = 0;
41+
bool applied = false;
42+
SubscriptionHandle? handle = null;
43+
44+
void OnConnected(DbConnection conn, Identity identity, string authToken)
45+
{
46+
Log.Debug($"Connected to {DBNAME} on {HOST}");
47+
handle = conn.SubscriptionBuilder()
48+
.OnApplied(OnSubscriptionApplied)
49+
.OnError((ctx, err) =>
50+
{
51+
throw err;
52+
})
53+
.Subscribe(["SELECT * FROM ExampleData"]);
54+
}
55+
56+
void OnSubscriptionApplied(SubscriptionEventContext context)
57+
{
58+
applied = true;
59+
60+
// Do some operations that alter row state;
61+
// we will check that everything is in sync in the callbacks for these reducer calls.
62+
var TOLERANCE = 0.00001f;
63+
foreach (var exampleData in context.Db.ExampleData.Iter())
64+
{
65+
if (exampleData.TestPass == 1)
66+
{
67+
List<string> errors = new List<string>();
68+
// This row should have had values set by default Attributes
69+
if (exampleData.DefaultString != "This is a default string") { errors.Add("DefaultString"); }
70+
if (exampleData.DefaultBool != true) { errors.Add("DefaultBool"); }
71+
if (exampleData.DefaultI8 != 2) { errors.Add("DefaultI8"); }
72+
if (exampleData.DefaultU8 != 2) { errors.Add("DefaultU8"); }
73+
if (exampleData.DefaultI16 != 2) { errors.Add("DefaultI16"); }
74+
if (exampleData.DefaultU16 != 2) { errors.Add("DefaultU16"); }
75+
if (exampleData.DefaultI32 != 2) { errors.Add("DefaultI32"); }
76+
if (exampleData.DefaultU32 != 2) { errors.Add("DefaultU32"); }
77+
if (exampleData.DefaultI64 != 2) { errors.Add("DefaultI64"); }
78+
if (exampleData.DefaultU64 != 2) { errors.Add("DefaultU64"); }
79+
if (exampleData.DefaultHex != 2) { errors.Add("DefaultHex"); }
80+
if (exampleData.DefaultBin != 2) { errors.Add("DefaultBin"); }
81+
if (Math.Abs(exampleData.DefaultF32 - 2.0f) > TOLERANCE) { errors.Add("DefaultF32"); }
82+
if (Math.Abs(exampleData.DefaultF64 - 2.0) > TOLERANCE) { errors.Add("DefaultF64"); }
83+
if (exampleData.DefaultEnum != MyEnum.SetByAttribute) { errors.Add("DefaultEnum"); }
84+
if (exampleData.DefaultNull != null) { errors.Add("DefaultNull"); }
85+
86+
if (errors.Count > 0)
87+
{
88+
var errorString = string.Join(", ", errors);
89+
Log.Info($"ExampleData with key {exampleData.Primary}: Error: Key added during initial test pass, newly added rows {errorString} were not set by default attributes");
90+
}
91+
else
92+
{
93+
Log.Info($"ExampleData with key {exampleData.Primary}: Success! Key added during initial test pass, newly added rows are all properly set by default attributes");
94+
}
95+
}
96+
else if (exampleData.TestPass == 2)
97+
{
98+
List<string> errors = new List<string>();
99+
// This row should have had values set by initialized values
100+
if (exampleData.DefaultString != "") { errors.Add("DefaultString"); }
101+
if (exampleData.DefaultBool != false) { errors.Add("DefaultBool"); }
102+
if (exampleData.DefaultI8 != 1) { errors.Add("DefaultI8"); }
103+
if (exampleData.DefaultU8 != 1) { errors.Add("DefaultU8"); }
104+
if (exampleData.DefaultI16 != 1) { errors.Add("DefaultI16"); }
105+
if (exampleData.DefaultU16 != 1) { errors.Add("DefaultU16"); }
106+
if (exampleData.DefaultI32 != 1) { errors.Add("DefaultI32"); }
107+
if (exampleData.DefaultU32 != 1) { errors.Add("DefaultU32"); }
108+
if (exampleData.DefaultI64 != 1) { errors.Add("DefaultI64"); }
109+
if (exampleData.DefaultU64 != 1) { errors.Add("DefaultU64"); }
110+
if (exampleData.DefaultHex != 1) { errors.Add("DefaultHex"); }
111+
if (exampleData.DefaultBin != 1) { errors.Add("DefaultBin"); }
112+
if (Math.Abs(exampleData.DefaultF32 - 1.0f) > TOLERANCE) { errors.Add("DefaultF32"); }
113+
if (Math.Abs(exampleData.DefaultF64 - 1.0) > TOLERANCE) { errors.Add("DefaultF64"); }
114+
if (exampleData.DefaultEnum != MyEnum.SetByInitalization) { errors.Add("DefaultEnum"); }
115+
if (exampleData.DefaultNull == null || exampleData.DefaultNull.X != 1) { errors.Add("DefaultNull"); }
116+
117+
if (errors.Count > 0)
118+
{
119+
var errorString = string.Join(", ", errors);
120+
Log.Info($"ExampleData with key {exampleData.Primary}: Error: Key added after republishing, newly added rows {errorString} were not set by initialized values");
121+
}
122+
else
123+
{
124+
Log.Error($"ExampleData with key {exampleData.Primary}: Success! Key added after republishing, newly added rows are all properly set by initialized values");
125+
}
126+
}
127+
}
128+
Log.Info("Evaluation of ExampleData in republishing test completed.");
129+
}
130+
131+
System.AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
132+
{
133+
Log.Exception($"Unhandled exception: {sender} {args}");
134+
Environment.Exit(1);
135+
};
136+
var db = ConnectToDB();
137+
Log.Info("Starting timer");
138+
const int TIMEOUT = 20; // seconds;
139+
var start = DateTime.Now;
140+
while (!applied || waiting > 0)
141+
{
142+
db.FrameTick();
143+
Thread.Sleep(100);
144+
if ((DateTime.Now - start).Seconds > TIMEOUT)
145+
{
146+
Log.Error($"Timeout, all events should have elapsed in {TIMEOUT} seconds!");
147+
Environment.Exit(1);
148+
}
149+
}
150+
Log.Info("Success");
151+
Environment.Exit(0);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="SpacetimeDB.ClientSDK" Version="1.5.0" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
2+
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
3+
4+
#nullable enable
5+
6+
using System;
7+
using SpacetimeDB.ClientApi;
8+
using System.Collections.Generic;
9+
using System.Runtime.Serialization;
10+
11+
namespace SpacetimeDB.Types
12+
{
13+
public sealed partial class RemoteReducers : RemoteBase
14+
{
15+
public delegate void InsertHandler(ReducerEventContext ctx, uint id);
16+
public event InsertHandler? OnInsert;
17+
18+
public void Insert(uint id)
19+
{
20+
conn.InternalCallReducer(new Reducer.Insert(id), this.SetCallReducerFlags.InsertFlags);
21+
}
22+
23+
public bool InvokeInsert(ReducerEventContext ctx, Reducer.Insert args)
24+
{
25+
if (OnInsert == null)
26+
{
27+
if (InternalOnUnhandledReducerError != null)
28+
{
29+
switch (ctx.Event.Status)
30+
{
31+
case Status.Failed(var reason): InternalOnUnhandledReducerError(ctx, new Exception(reason)); break;
32+
case Status.OutOfEnergy(var _): InternalOnUnhandledReducerError(ctx, new Exception("out of energy")); break;
33+
}
34+
}
35+
return false;
36+
}
37+
OnInsert(
38+
ctx,
39+
args.Id
40+
);
41+
return true;
42+
}
43+
}
44+
45+
public abstract partial class Reducer
46+
{
47+
[SpacetimeDB.Type]
48+
[DataContract]
49+
public sealed partial class Insert : Reducer, IReducerArgs
50+
{
51+
[DataMember(Name = "id")]
52+
public uint Id;
53+
54+
public Insert(uint Id)
55+
{
56+
this.Id = Id;
57+
}
58+
59+
public Insert()
60+
{
61+
}
62+
63+
string IReducerArgs.ReducerName => "Insert";
64+
}
65+
}
66+
67+
public sealed partial class SetReducerFlags
68+
{
69+
internal CallReducerFlags InsertFlags;
70+
public void Insert(CallReducerFlags flags) => InsertFlags = flags;
71+
}
72+
}

0 commit comments

Comments
 (0)