1use super::util::fmt_fn;
3
4use std::fmt::{self, Write};
5use std::ops::Deref;
6
7use super::code_indenter::CodeIndenter;
8use super::Lang;
9use crate::indent_scope;
10use crate::util::{
11 collect_case, is_reducer_invokable, iter_indexes, iter_reducers, iter_tables, print_auto_generated_file_comment,
12 type_ref_name,
13};
14use convert_case::{Case, Casing};
15use spacetimedb_lib::sats::layout::PrimitiveType;
16use spacetimedb_primitives::ColId;
17use spacetimedb_schema::def::{BTreeAlgorithm, IndexAlgorithm, ModuleDef, TableDef, TypeDef};
18use spacetimedb_schema::identifier::Identifier;
19use spacetimedb_schema::schema::{Schema, TableSchema};
20use spacetimedb_schema::type_for_generate::{
21 AlgebraicTypeDef, AlgebraicTypeUse, PlainEnumTypeDef, ProductTypeDef, SumTypeDef, TypespaceForGenerate,
22};
23
24const INDENT: &str = " ";
25
26const REDUCER_EVENTS: &str = r#"
27 public interface IRemoteDbContext : IDbContext<RemoteTables, RemoteReducers, SetReducerFlags, SubscriptionBuilder> {
28 public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError;
29 }
30
31 public sealed class EventContext : IEventContext, IRemoteDbContext
32 {
33 private readonly DbConnection conn;
34
35 /// <summary>
36 /// The event that caused this callback to run.
37 /// </summary>
38 public readonly Event<Reducer> Event;
39
40 /// <summary>
41 /// Access to tables in the client cache, which stores a read-only replica of the remote database state.
42 ///
43 /// The returned <c>DbView</c> will have a method to access each table defined by the module.
44 /// </summary>
45 public RemoteTables Db => conn.Db;
46 /// <summary>
47 /// Access to reducers defined by the module.
48 ///
49 /// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
50 /// plus methods for adding and removing callbacks on each of those reducers.
51 /// </summary>
52 public RemoteReducers Reducers => conn.Reducers;
53 /// <summary>
54 /// Access to setters for per-reducer flags.
55 ///
56 /// The returned <c>SetReducerFlags</c> will have a method to invoke,
57 /// for each reducer defined by the module,
58 /// which call-flags for the reducer can be set.
59 /// </summary>
60 public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
61 /// <summary>
62 /// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
63 /// </summary>
64 public bool IsActive => conn.IsActive;
65 /// <summary>
66 /// Close the connection.
67 ///
68 /// Throws an error if the connection is already closed.
69 /// </summary>
70 public void Disconnect() {
71 conn.Disconnect();
72 }
73 /// <summary>
74 /// Start building a subscription.
75 /// </summary>
76 /// <returns>A builder-pattern constructor for subscribing to queries,
77 /// causing matching rows to be replicated into the client cache.</returns>
78 public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
79 /// <summary>
80 /// Get the <c>Identity</c> of this connection.
81 ///
82 /// This method returns null if the connection was constructed anonymously
83 /// and we have not yet received our newly-generated <c>Identity</c> from the host.
84 /// </summary>
85 public Identity? Identity => conn.Identity;
86 /// <summary>
87 /// Get this connection's <c>ConnectionId</c>.
88 /// </summary>
89 public ConnectionId ConnectionId => conn.ConnectionId;
90 /// <summary>
91 /// Register a callback to be called when a reducer with no handler returns an error.
92 /// </summary>
93 public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError {
94 add => Reducers.InternalOnUnhandledReducerError += value;
95 remove => Reducers.InternalOnUnhandledReducerError -= value;
96 }
97
98 internal EventContext(DbConnection conn, Event<Reducer> Event)
99 {
100 this.conn = conn;
101 this.Event = Event;
102 }
103 }
104
105 public sealed class ReducerEventContext : IReducerEventContext, IRemoteDbContext
106 {
107 private readonly DbConnection conn;
108 /// <summary>
109 /// The reducer event that caused this callback to run.
110 /// </summary>
111 public readonly ReducerEvent<Reducer> Event;
112
113 /// <summary>
114 /// Access to tables in the client cache, which stores a read-only replica of the remote database state.
115 ///
116 /// The returned <c>DbView</c> will have a method to access each table defined by the module.
117 /// </summary>
118 public RemoteTables Db => conn.Db;
119 /// <summary>
120 /// Access to reducers defined by the module.
121 ///
122 /// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
123 /// plus methods for adding and removing callbacks on each of those reducers.
124 /// </summary>
125 public RemoteReducers Reducers => conn.Reducers;
126 /// <summary>
127 /// Access to setters for per-reducer flags.
128 ///
129 /// The returned <c>SetReducerFlags</c> will have a method to invoke,
130 /// for each reducer defined by the module,
131 /// which call-flags for the reducer can be set.
132 /// </summary>
133 public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
134 /// <summary>
135 /// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
136 /// </summary>
137 public bool IsActive => conn.IsActive;
138 /// <summary>
139 /// Close the connection.
140 ///
141 /// Throws an error if the connection is already closed.
142 /// </summary>
143 public void Disconnect() {
144 conn.Disconnect();
145 }
146 /// <summary>
147 /// Start building a subscription.
148 /// </summary>
149 /// <returns>A builder-pattern constructor for subscribing to queries,
150 /// causing matching rows to be replicated into the client cache.</returns>
151 public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
152 /// <summary>
153 /// Get the <c>Identity</c> of this connection.
154 ///
155 /// This method returns null if the connection was constructed anonymously
156 /// and we have not yet received our newly-generated <c>Identity</c> from the host.
157 /// </summary>
158 public Identity? Identity => conn.Identity;
159 /// <summary>
160 /// Get this connection's <c>ConnectionId</c>.
161 /// </summary>
162 public ConnectionId ConnectionId => conn.ConnectionId;
163 /// <summary>
164 /// Register a callback to be called when a reducer with no handler returns an error.
165 /// </summary>
166 public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError {
167 add => Reducers.InternalOnUnhandledReducerError += value;
168 remove => Reducers.InternalOnUnhandledReducerError -= value;
169 }
170
171 internal ReducerEventContext(DbConnection conn, ReducerEvent<Reducer> reducerEvent)
172 {
173 this.conn = conn;
174 Event = reducerEvent;
175 }
176 }
177
178 public sealed class ErrorContext : IErrorContext, IRemoteDbContext
179 {
180 private readonly DbConnection conn;
181 /// <summary>
182 /// The <c>Exception</c> that caused this error callback to be run.
183 /// </summary>
184 public readonly Exception Event;
185 Exception IErrorContext.Event {
186 get {
187 return Event;
188 }
189 }
190
191 /// <summary>
192 /// Access to tables in the client cache, which stores a read-only replica of the remote database state.
193 ///
194 /// The returned <c>DbView</c> will have a method to access each table defined by the module.
195 /// </summary>
196 public RemoteTables Db => conn.Db;
197 /// <summary>
198 /// Access to reducers defined by the module.
199 ///
200 /// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
201 /// plus methods for adding and removing callbacks on each of those reducers.
202 /// </summary>
203 public RemoteReducers Reducers => conn.Reducers;
204 /// <summary>
205 /// Access to setters for per-reducer flags.
206 ///
207 /// The returned <c>SetReducerFlags</c> will have a method to invoke,
208 /// for each reducer defined by the module,
209 /// which call-flags for the reducer can be set.
210 /// </summary>
211 public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
212 /// <summary>
213 /// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
214 /// </summary>
215 public bool IsActive => conn.IsActive;
216 /// <summary>
217 /// Close the connection.
218 ///
219 /// Throws an error if the connection is already closed.
220 /// </summary>
221 public void Disconnect() {
222 conn.Disconnect();
223 }
224 /// <summary>
225 /// Start building a subscription.
226 /// </summary>
227 /// <returns>A builder-pattern constructor for subscribing to queries,
228 /// causing matching rows to be replicated into the client cache.</returns>
229 public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
230 /// <summary>
231 /// Get the <c>Identity</c> of this connection.
232 ///
233 /// This method returns null if the connection was constructed anonymously
234 /// and we have not yet received our newly-generated <c>Identity</c> from the host.
235 /// </summary>
236 public Identity? Identity => conn.Identity;
237 /// <summary>
238 /// Get this connection's <c>ConnectionId</c>.
239 /// </summary>
240 public ConnectionId ConnectionId => conn.ConnectionId;
241 /// <summary>
242 /// Register a callback to be called when a reducer with no handler returns an error.
243 /// </summary>
244 public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError {
245 add => Reducers.InternalOnUnhandledReducerError += value;
246 remove => Reducers.InternalOnUnhandledReducerError -= value;
247 }
248
249 internal ErrorContext(DbConnection conn, Exception error)
250 {
251 this.conn = conn;
252 Event = error;
253 }
254 }
255
256 public sealed class SubscriptionEventContext : ISubscriptionEventContext, IRemoteDbContext
257 {
258 private readonly DbConnection conn;
259
260 /// <summary>
261 /// Access to tables in the client cache, which stores a read-only replica of the remote database state.
262 ///
263 /// The returned <c>DbView</c> will have a method to access each table defined by the module.
264 /// </summary>
265 public RemoteTables Db => conn.Db;
266 /// <summary>
267 /// Access to reducers defined by the module.
268 ///
269 /// The returned <c>RemoteReducers</c> will have a method to invoke each reducer defined by the module,
270 /// plus methods for adding and removing callbacks on each of those reducers.
271 /// </summary>
272 public RemoteReducers Reducers => conn.Reducers;
273 /// <summary>
274 /// Access to setters for per-reducer flags.
275 ///
276 /// The returned <c>SetReducerFlags</c> will have a method to invoke,
277 /// for each reducer defined by the module,
278 /// which call-flags for the reducer can be set.
279 /// </summary>
280 public SetReducerFlags SetReducerFlags => conn.SetReducerFlags;
281 /// <summary>
282 /// Returns <c>true</c> if the connection is active, i.e. has not yet disconnected.
283 /// </summary>
284 public bool IsActive => conn.IsActive;
285 /// <summary>
286 /// Close the connection.
287 ///
288 /// Throws an error if the connection is already closed.
289 /// </summary>
290 public void Disconnect() {
291 conn.Disconnect();
292 }
293 /// <summary>
294 /// Start building a subscription.
295 /// </summary>
296 /// <returns>A builder-pattern constructor for subscribing to queries,
297 /// causing matching rows to be replicated into the client cache.</returns>
298 public SubscriptionBuilder SubscriptionBuilder() => conn.SubscriptionBuilder();
299 /// <summary>
300 /// Get the <c>Identity</c> of this connection.
301 ///
302 /// This method returns null if the connection was constructed anonymously
303 /// and we have not yet received our newly-generated <c>Identity</c> from the host.
304 /// </summary>
305 public Identity? Identity => conn.Identity;
306 /// <summary>
307 /// Get this connection's <c>ConnectionId</c>.
308 /// </summary>
309 public ConnectionId ConnectionId => conn.ConnectionId;
310 /// <summary>
311 /// Register a callback to be called when a reducer with no handler returns an error.
312 /// </summary>
313 public event Action<ReducerEventContext, Exception>? OnUnhandledReducerError {
314 add => Reducers.InternalOnUnhandledReducerError += value;
315 remove => Reducers.InternalOnUnhandledReducerError -= value;
316 }
317
318 internal SubscriptionEventContext(DbConnection conn)
319 {
320 this.conn = conn;
321 }
322 }
323
324 /// <summary>
325 /// Builder-pattern constructor for subscription queries.
326 /// </summary>
327 public sealed class SubscriptionBuilder
328 {
329 private readonly IDbConnection conn;
330
331 private event Action<SubscriptionEventContext>? Applied;
332 private event Action<ErrorContext, Exception>? Error;
333
334 /// <summary>
335 /// Private API, use <c>conn.SubscriptionBuilder()</c> instead.
336 /// </summary>
337 public SubscriptionBuilder(IDbConnection conn)
338 {
339 this.conn = conn;
340 }
341
342 /// <summary>
343 /// Register a callback to run when the subscription is applied.
344 /// </summary>
345 public SubscriptionBuilder OnApplied(
346 Action<SubscriptionEventContext> callback
347 )
348 {
349 Applied += callback;
350 return this;
351 }
352
353 /// <summary>
354 /// Register a callback to run when the subscription fails.
355 ///
356 /// Note that this callback may run either when attempting to apply the subscription,
357 /// in which case <c>Self::on_applied</c> will never run,
358 /// or later during the subscription's lifetime if the module's interface changes,
359 /// in which case <c>Self::on_applied</c> may have already run.
360 /// </summary>
361 public SubscriptionBuilder OnError(
362 Action<ErrorContext, Exception> callback
363 )
364 {
365 Error += callback;
366 return this;
367 }
368
369 /// <summary>
370 /// Subscribe to the following SQL queries.
371 ///
372 /// This method returns immediately, with the data not yet added to the DbConnection.
373 /// The provided callbacks will be invoked once the data is returned from the remote server.
374 /// Data from all the provided queries will be returned at the same time.
375 ///
376 /// See the SpacetimeDB SQL docs for more information on SQL syntax:
377 /// <a href="https://spacetimedb.com/docs/sql">https://spacetimedb.com/docs/sql</a>
378 /// </summary>
379 public SubscriptionHandle Subscribe(
380 string[] querySqls
381 ) => new(conn, Applied, Error, querySqls);
382
383 /// <summary>
384 /// Subscribe to all rows from all tables.
385 ///
386 /// This method is intended as a convenience
387 /// for applications where client-side memory use and network bandwidth are not concerns.
388 /// Applications where these resources are a constraint
389 /// should register more precise queries via <c>Self.Subscribe</c>
390 /// in order to replicate only the subset of data which the client needs to function.
391 ///
392 /// This method should not be combined with <c>Self.Subscribe</c> on the same <c>DbConnection</c>.
393 /// A connection may either <c>Self.Subscribe</c> to particular queries,
394 /// or <c>Self.SubscribeToAllTables</c>, but not both.
395 /// Attempting to call <c>Self.Subscribe</c>
396 /// on a <c>DbConnection</c> that has previously used <c>Self.SubscribeToAllTables</c>,
397 /// or vice versa, may misbehave in any number of ways,
398 /// including dropping subscriptions, corrupting the client cache, or panicking.
399 /// </summary>
400 public void SubscribeToAllTables()
401 {
402 // Make sure we use the legacy handle constructor here, even though there's only 1 query.
403 // We drop the error handler, since it can't be called for legacy subscriptions.
404 new SubscriptionHandle(
405 conn,
406 Applied,
407 new string[] { "SELECT * FROM *" }
408 );
409 }
410 }
411
412 public sealed class SubscriptionHandle : SubscriptionHandleBase<SubscriptionEventContext, ErrorContext> {
413 /// <summary>
414 /// Internal API. Construct <c>SubscriptionHandle</c>s using <c>conn.SubscriptionBuilder</c>.
415 /// </summary>
416 public SubscriptionHandle(IDbConnection conn, Action<SubscriptionEventContext>? onApplied, string[] querySqls) : base(conn, onApplied, querySqls)
417 { }
418
419 /// <summary>
420 /// Internal API. Construct <c>SubscriptionHandle</c>s using <c>conn.SubscriptionBuilder</c>.
421 /// </summary>
422 public SubscriptionHandle(
423 IDbConnection conn,
424 Action<SubscriptionEventContext>? onApplied,
425 Action<ErrorContext, Exception>? onError,
426 string[] querySqls
427 ) : base(conn, onApplied, onError, querySqls)
428 { }
429 }
430"#;
431
432pub struct Csharp<'opts> {
433 pub namespace: &'opts str,
434}
435
436impl Lang for Csharp<'_> {
437 fn table_filename(&self, _module: &ModuleDef, table: &TableDef) -> String {
438 format!("Tables/{}.g.cs", table.name.deref().to_case(Case::Pascal))
439 }
440
441 fn type_filename(&self, type_name: &spacetimedb_schema::def::ScopedTypeName) -> String {
442 format!("Types/{}.g.cs", collect_case(Case::Pascal, type_name.name_segments()))
443 }
444
445 fn reducer_filename(&self, reducer_name: &Identifier) -> String {
446 format!("Reducers/{}.g.cs", reducer_name.deref().to_case(Case::Pascal))
447 }
448
449 fn generate_table(&self, module: &ModuleDef, table: &TableDef) -> String {
450 let mut output = CsharpAutogen::new(
451 self.namespace,
452 &[
453 "SpacetimeDB.BSATN",
454 "SpacetimeDB.ClientApi",
455 "System.Collections.Generic",
456 "System.Runtime.Serialization",
457 ],
458 );
459
460 writeln!(output, "public sealed partial class RemoteTables");
461 indented_block(&mut output, |output| {
462 let schema = TableSchema::from_module_def(module, table, (), 0.into())
463 .validated()
464 .expect("Failed to generate table due to validation errors");
465 let csharp_table_name = table.name.deref().to_case(Case::Pascal);
466 let csharp_table_class_name = csharp_table_name.clone() + "Handle";
467 let table_type = type_ref_name(module, table.product_type_ref);
468
469 writeln!(
470 output,
471 "public sealed class {csharp_table_class_name} : RemoteTableHandle<EventContext, {table_type}>"
472 );
473 indented_block(output, |output| {
474 writeln!(
475 output,
476 "protected override string RemoteTableName => \"{}\";",
477 table.name
478 );
479 writeln!(output);
480
481 let product_type = module.typespace_for_generate()[table.product_type_ref]
483 .as_product()
484 .unwrap();
485
486 let mut index_names = Vec::new();
487
488 for idx in iter_indexes(table) {
489 let Some(accessor_name) = idx.accessor_name.as_ref() else {
490 continue;
492 };
493
494 match &idx.algorithm {
495 IndexAlgorithm::BTree(BTreeAlgorithm { columns }) => {
496 let get_csharp_field_name_and_type = |col_pos: ColId| {
497 let (field_name, field_type) = &product_type.elements[col_pos.idx()];
498 let csharp_field_name_pascal = field_name.deref().to_case(Case::Pascal);
499 let csharp_field_type = ty_fmt(module, field_type);
500 (csharp_field_name_pascal, csharp_field_type)
501 };
502
503 let (row_to_key, key_type) = match columns.as_singleton() {
504 Some(col_pos) => {
505 let (field_name, field_type) = get_csharp_field_name_and_type(col_pos);
506 (format!("row.{field_name}"), field_type.to_string())
507 }
508 None => {
509 let mut key_accessors = Vec::new();
510 let mut key_type_elems = Vec::new();
511 for (field_name, field_type) in columns.iter().map(get_csharp_field_name_and_type) {
512 key_accessors.push(format!("row.{field_name}"));
513 key_type_elems.push(format!("{field_type} {field_name}"));
514 }
515 (
516 format!("({})", key_accessors.join(", ")),
517 format!("({})", key_type_elems.join(", ")),
518 )
519 }
520 };
521
522 let csharp_index_name = accessor_name.deref().to_case(Case::Pascal);
523
524 let mut csharp_index_class_name = csharp_index_name.clone();
525 let csharp_index_base_class_name = if schema.is_unique(columns) {
526 csharp_index_class_name += "UniqueIndex";
527 "UniqueIndexBase"
528 } else {
529 csharp_index_class_name += "Index";
530 "BTreeIndexBase"
531 };
532
533 writeln!(output, "public sealed class {csharp_index_class_name} : {csharp_index_base_class_name}<{key_type}>");
534 indented_block(output, |output| {
535 writeln!(
536 output,
537 "protected override {key_type} GetKey({table_type} row) => {row_to_key};"
538 );
539 writeln!(output);
540 writeln!(output, "public {csharp_index_class_name}({csharp_table_class_name} table) : base(table) {{ }}");
541 });
542 writeln!(output);
543 writeln!(output, "public readonly {csharp_index_class_name} {csharp_index_name};");
544 writeln!(output);
545
546 index_names.push(csharp_index_name);
547 }
548 _ => todo!(),
549 }
550 }
551
552 writeln!(
553 output,
554 "internal {csharp_table_class_name}(DbConnection conn) : base(conn)"
555 );
556 indented_block(output, |output| {
557 for csharp_index_name in &index_names {
558 writeln!(output, "{csharp_index_name} = new(this);");
559 }
560 });
561
562 if let Some(primary_col_index) = schema.pk() {
563 writeln!(output);
564 writeln!(
565 output,
566 "protected override object GetPrimaryKey({table_type} row) => row.{col_name_pascal_case};",
567 col_name_pascal_case = primary_col_index.col_name.deref().to_case(Case::Pascal)
568 );
569 }
570 });
571 writeln!(output);
572 writeln!(output, "public readonly {csharp_table_class_name} {csharp_table_name};");
573 });
574
575 output.into_inner()
576 }
577
578 fn generate_type(&self, module: &ModuleDef, typ: &TypeDef) -> String {
579 let name = collect_case(Case::Pascal, typ.name.name_segments());
580 match &module.typespace_for_generate()[typ.ty] {
581 AlgebraicTypeDef::Sum(sum) => autogen_csharp_sum(module, name, sum, self.namespace),
582 AlgebraicTypeDef::Product(prod) => autogen_csharp_tuple(module, name, prod, self.namespace),
583 AlgebraicTypeDef::PlainEnum(plain_enum) => autogen_csharp_plain_enum(name, plain_enum, self.namespace),
584 }
585 }
586
587 fn generate_reducer(&self, module: &ModuleDef, reducer: &spacetimedb_schema::def::ReducerDef) -> String {
588 let mut output = CsharpAutogen::new(
589 self.namespace,
590 &[
591 "SpacetimeDB.ClientApi",
592 "System.Collections.Generic",
593 "System.Runtime.Serialization",
594 ],
595 );
596
597 writeln!(output, "public sealed partial class RemoteReducers : RemoteBase");
598 indented_block(&mut output, |output| {
599 let func_name_pascal_case = reducer.name.deref().to_case(Case::Pascal);
600 let delegate_separator = if reducer.params_for_generate.elements.is_empty() {
601 ""
602 } else {
603 ", "
604 };
605
606 let mut func_params: String = String::new();
607 let mut func_args: String = String::new();
608
609 for (arg_i, (arg_name, arg_ty)) in reducer.params_for_generate.into_iter().enumerate() {
610 if arg_i != 0 {
611 func_params.push_str(", ");
612 func_args.push_str(", ");
613 }
614
615 let arg_type_str = ty_fmt(module, arg_ty);
616 let arg_name = arg_name.deref().to_case(Case::Camel);
617
618 write!(func_params, "{arg_type_str} {arg_name}").unwrap();
619 write!(func_args, "{arg_name}").unwrap();
620 }
621
622 writeln!(
623 output,
624 "public delegate void {func_name_pascal_case}Handler(ReducerEventContext ctx{delegate_separator}{func_params});"
625 );
626 writeln!(
627 output,
628 "public event {func_name_pascal_case}Handler? On{func_name_pascal_case};"
629 );
630 writeln!(output);
631
632 if is_reducer_invokable(reducer) {
633 writeln!(output, "public void {func_name_pascal_case}({func_params})");
634 indented_block(output, |output| {
635 writeln!(
636 output,
637 "conn.InternalCallReducer(new Reducer.{func_name_pascal_case}({func_args}), this.SetCallReducerFlags.{func_name_pascal_case}Flags);"
638 );
639 });
640 writeln!(output);
641 }
642
643 writeln!(
644 output,
645 "public bool Invoke{func_name_pascal_case}(ReducerEventContext ctx, Reducer.{func_name_pascal_case} args)"
646 );
647 indented_block(output, |output| {
648 writeln!(output, "if (On{func_name_pascal_case} == null)");
649 indented_block(output, |output| {
650 writeln!(output, "if (InternalOnUnhandledReducerError != null)");
651 indented_block(output, |output| {
652 writeln!(output, "switch(ctx.Event.Status)");
653 indented_block(output, |output| {
654 writeln!(output, "case Status.Failed(var reason): InternalOnUnhandledReducerError(ctx, new Exception(reason)); break;");
655 writeln!(output, "case Status.OutOfEnergy(var _): InternalOnUnhandledReducerError(ctx, new Exception(\"out of energy\")); break;");
656 });
657 });
658 writeln!(output, "return false;");
659 });
660
661 writeln!(output, "On{func_name_pascal_case}(");
662 {
664 indent_scope!(output);
665 write!(output, "ctx");
666 for (arg_name, _) in &reducer.params_for_generate {
667 writeln!(output, ",");
668 let arg_name = arg_name.deref().to_case(Case::Pascal);
669 write!(output, "args.{arg_name}");
670 }
671 writeln!(output);
672 }
673 writeln!(output, ");");
674 writeln!(output, "return true;");
675 });
676 });
677
678 writeln!(output);
679
680 writeln!(output, "public abstract partial class Reducer");
681 indented_block(&mut output, |output| {
682 autogen_csharp_product_common(
683 module,
684 output,
685 reducer.name.deref().to_case(Case::Pascal),
686 &reducer.params_for_generate,
687 "Reducer, IReducerArgs",
688 |output| {
689 if !reducer.params_for_generate.elements.is_empty() {
690 writeln!(output);
691 }
692 writeln!(output, "string IReducerArgs.ReducerName => \"{}\";", reducer.name);
693 },
694 );
695 });
696
697 if is_reducer_invokable(reducer) {
698 writeln!(output);
699 writeln!(output, "public sealed partial class SetReducerFlags");
700 indented_block(&mut output, |output| {
701 let func_name_pascal_case = reducer.name.deref().to_case(Case::Pascal);
702 writeln!(output, "internal CallReducerFlags {func_name_pascal_case}Flags;");
703 writeln!(output, "public void {func_name_pascal_case}(CallReducerFlags flags) => {func_name_pascal_case}Flags = flags;");
704 });
705 }
706
707 output.into_inner()
708 }
709
710 fn generate_globals(&self, module: &ModuleDef) -> Vec<(String, String)> {
711 let mut output = CsharpAutogen::new(
712 self.namespace,
713 &[
714 "SpacetimeDB.ClientApi",
715 "System.Collections.Generic",
716 "System.Runtime.Serialization",
717 ],
718 );
719
720 writeln!(output, "public sealed partial class RemoteReducers : RemoteBase");
721 indented_block(&mut output, |output| {
722 writeln!(
723 output,
724 "internal RemoteReducers(DbConnection conn, SetReducerFlags flags) : base(conn) => SetCallReducerFlags = flags;"
725 );
726 writeln!(output, "internal readonly SetReducerFlags SetCallReducerFlags;");
727 writeln!(
728 output,
729 "internal event Action<ReducerEventContext, Exception>? InternalOnUnhandledReducerError;"
730 )
731 });
732 writeln!(output);
733
734 writeln!(output, "public sealed partial class RemoteTables : RemoteTablesBase");
735 indented_block(&mut output, |output| {
736 writeln!(output, "public RemoteTables(DbConnection conn)");
737 indented_block(output, |output| {
738 for table in iter_tables(module) {
739 writeln!(
740 output,
741 "AddTable({} = new(conn));",
742 table.name.deref().to_case(Case::Pascal)
743 );
744 }
745 });
746 });
747 writeln!(output);
748
749 writeln!(output, "public sealed partial class SetReducerFlags {{ }}");
750
751 writeln!(output, "{REDUCER_EVENTS}");
752
753 writeln!(output, "public abstract partial class Reducer");
754 indented_block(&mut output, |output| {
755 writeln!(output, "private Reducer() {{ }}");
757 });
758 writeln!(output);
759
760 writeln!(
761 output,
762 "public sealed class DbConnection : DbConnectionBase<DbConnection, RemoteTables, Reducer>"
763 );
764 indented_block(&mut output, |output: &mut CodeIndenter<String>| {
765 writeln!(output, "public override RemoteTables Db {{ get; }}");
766 writeln!(output, "public readonly RemoteReducers Reducers;");
767 writeln!(output, "public readonly SetReducerFlags SetReducerFlags = new();");
768 writeln!(output);
769
770 writeln!(output, "public DbConnection()");
771 indented_block(output, |output| {
772 writeln!(output, "Db = new(this);");
773 writeln!(output, "Reducers = new(this, SetReducerFlags);");
774 });
775 writeln!(output);
776
777 writeln!(output, "protected override Reducer ToReducer(TransactionUpdate update)");
778 indented_block(output, |output| {
779 writeln!(output, "var encodedArgs = update.ReducerCall.Args;");
780 writeln!(output, "return update.ReducerCall.ReducerName switch {{");
781 {
782 indent_scope!(output);
783 for reducer in iter_reducers(module) {
784 let reducer_str_name = &reducer.name;
785 let reducer_name = reducer.name.deref().to_case(Case::Pascal);
786 writeln!(
787 output,
788 "\"{reducer_str_name}\" => BSATNHelpers.Decode<Reducer.{reducer_name}>(encodedArgs),"
789 );
790 }
791 writeln!(
792 output,
793 r#"var reducer => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {{reducer}}")"#
794 );
795 }
796 writeln!(output, "}};");
797 });
798 writeln!(output);
799
800 writeln!(
801 output,
802 "protected override IEventContext ToEventContext(Event<Reducer> Event) =>"
803 );
804 writeln!(output, "new EventContext(this, Event);");
805 writeln!(output);
806
807 writeln!(
808 output,
809 "protected override IReducerEventContext ToReducerEventContext(ReducerEvent<Reducer> reducerEvent) =>"
810 );
811 writeln!(output, "new ReducerEventContext(this, reducerEvent);");
812 writeln!(output);
813
814 writeln!(
815 output,
816 "protected override ISubscriptionEventContext MakeSubscriptionEventContext() =>"
817 );
818 writeln!(output, "new SubscriptionEventContext(this);");
819 writeln!(output);
820
821 writeln!(
822 output,
823 "protected override IErrorContext ToErrorContext(Exception exception) =>"
824 );
825 writeln!(output, "new ErrorContext(this, exception);");
826 writeln!(output);
827
828 writeln!(
829 output,
830 "protected override bool Dispatch(IReducerEventContext context, Reducer reducer)"
831 );
832 indented_block(output, |output| {
833 writeln!(output, "var eventContext = (ReducerEventContext)context;");
834 writeln!(output, "return reducer switch {{");
835 {
836 indent_scope!(output);
837 for reducer_name in iter_reducers(module).map(|r| r.name.deref().to_case(Case::Pascal)) {
838 writeln!(
839 output,
840 "Reducer.{reducer_name} args => Reducers.Invoke{reducer_name}(eventContext, args),"
841 );
842 }
843 writeln!(
844 output,
845 r#"_ => throw new ArgumentOutOfRangeException("Reducer", $"Unknown reducer {{reducer}}")"#
846 );
847 }
848 writeln!(output, "}};");
849 });
850 writeln!(output);
851
852 writeln!(output, "public SubscriptionBuilder SubscriptionBuilder() => new(this);");
853 writeln!(
854 output,
855 "public event Action<ReducerEventContext, Exception> OnUnhandledReducerError"
856 );
857 indented_block(output, |output| {
858 writeln!(output, "add => Reducers.InternalOnUnhandledReducerError += value;");
859 writeln!(output, "remove => Reducers.InternalOnUnhandledReducerError -= value;");
860 });
861 });
862
863 vec![("SpacetimeDBClient.g.cs".to_owned(), output.into_inner())]
864 }
865}
866
867fn ty_fmt<'a>(module: &'a ModuleDef, ty: &'a AlgebraicTypeUse) -> impl fmt::Display + 'a {
868 fmt_fn(move |f| match ty {
869 AlgebraicTypeUse::Identity => f.write_str("SpacetimeDB.Identity"),
870 AlgebraicTypeUse::ConnectionId => f.write_str("SpacetimeDB.ConnectionId"),
871 AlgebraicTypeUse::ScheduleAt => f.write_str("SpacetimeDB.ScheduleAt"),
872 AlgebraicTypeUse::Timestamp => f.write_str("SpacetimeDB.Timestamp"),
873 AlgebraicTypeUse::TimeDuration => f.write_str("SpacetimeDB.TimeDuration"),
874 AlgebraicTypeUse::Unit => f.write_str("SpacetimeDB.Unit"),
875 AlgebraicTypeUse::Option(inner_ty) => write!(f, "{}?", ty_fmt(module, inner_ty)),
876 AlgebraicTypeUse::Array(elem_ty) => write!(f, "System.Collections.Generic.List<{}>", ty_fmt(module, elem_ty)),
877 AlgebraicTypeUse::String => f.write_str("string"),
878 AlgebraicTypeUse::Ref(r) => f.write_str(&type_ref_name(module, *r)),
879 AlgebraicTypeUse::Primitive(prim) => f.write_str(match prim {
880 PrimitiveType::Bool => "bool",
881 PrimitiveType::I8 => "sbyte",
882 PrimitiveType::U8 => "byte",
883 PrimitiveType::I16 => "short",
884 PrimitiveType::U16 => "ushort",
885 PrimitiveType::I32 => "int",
886 PrimitiveType::U32 => "uint",
887 PrimitiveType::I64 => "long",
888 PrimitiveType::U64 => "ulong",
889 PrimitiveType::I128 => "I128",
890 PrimitiveType::U128 => "U128",
891 PrimitiveType::I256 => "I256",
892 PrimitiveType::U256 => "U256",
893 PrimitiveType::F32 => "float",
894 PrimitiveType::F64 => "double",
895 }),
896 AlgebraicTypeUse::Never => unimplemented!(),
897 })
898}
899
900fn default_init(ctx: &TypespaceForGenerate, ty: &AlgebraicTypeUse) -> Option<&'static str> {
901 match ty {
902 AlgebraicTypeUse::Option(_) => None,
904 AlgebraicTypeUse::Ref(r) => match &ctx[*r] {
905 AlgebraicTypeDef::Sum(_) => Some("null!"),
907 AlgebraicTypeDef::PlainEnum(_) => None,
909 AlgebraicTypeDef::Product(_) => Some("new()"),
910 },
911 AlgebraicTypeUse::ScheduleAt => Some("null!"),
913 AlgebraicTypeUse::Array(_) => Some("new()"),
914 AlgebraicTypeUse::String => Some(r#""""#),
916 AlgebraicTypeUse::Primitive(_) => None,
918 AlgebraicTypeUse::Unit
920 | AlgebraicTypeUse::Identity
921 | AlgebraicTypeUse::ConnectionId
922 | AlgebraicTypeUse::Timestamp
923 | AlgebraicTypeUse::TimeDuration => None,
924 AlgebraicTypeUse::Never => unimplemented!("never types are not yet supported in C# output"),
925 }
926}
927
928struct CsharpAutogen {
929 output: CodeIndenter<String>,
930}
931
932impl Deref for CsharpAutogen {
933 type Target = CodeIndenter<String>;
934
935 fn deref(&self) -> &Self::Target {
936 &self.output
937 }
938}
939
940impl std::ops::DerefMut for CsharpAutogen {
941 fn deref_mut(&mut self) -> &mut Self::Target {
942 &mut self.output
943 }
944}
945
946impl CsharpAutogen {
947 pub fn new(namespace: &str, extra_usings: &[&str]) -> Self {
948 let mut output = CodeIndenter::new(String::new(), INDENT);
949
950 print_auto_generated_file_comment(&mut output);
951
952 writeln!(output, "#nullable enable");
953 writeln!(output);
954
955 writeln!(output, "using System;");
956 if namespace
958 .split('.')
959 .next()
960 .expect("split always returns at least one string")
961 != "SpacetimeDB"
962 {
963 writeln!(output, "using SpacetimeDB;");
964 }
965 for extra_using in extra_usings {
966 writeln!(output, "using {extra_using};");
967 }
968 writeln!(output);
969
970 writeln!(output, "namespace {namespace}");
971 writeln!(output, "{{");
972 output.indent(1);
973
974 Self { output }
975 }
976
977 pub fn into_inner(mut self) -> String {
978 self.dedent(1);
979 writeln!(self, "}}");
980
981 self.output.into_inner()
982 }
983}
984
985fn autogen_csharp_sum(module: &ModuleDef, sum_type_name: String, sum_type: &SumTypeDef, namespace: &str) -> String {
986 let mut output = CsharpAutogen::new(namespace, &[]);
987
988 writeln!(output, "[SpacetimeDB.Type]");
989 write!(
990 output,
991 "public partial record {sum_type_name} : SpacetimeDB.TaggedEnum<("
992 );
993 {
994 indent_scope!(output);
995 for (i, (variant_name, variant_ty)) in sum_type.variants.iter().enumerate() {
996 if i != 0 {
997 write!(output, ",");
998 }
999 writeln!(output);
1000 write!(output, "{} {variant_name}", ty_fmt(module, variant_ty));
1001 }
1002 match sum_type.variants.len() {
1004 0 => {
1005 writeln!(output);
1006 writeln!(output, "SpacetimeDB.Unit _Reserved1,");
1007 write!(output, "SpacetimeDB.Unit _Reserved2");
1008 }
1009 1 => {
1010 writeln!(output, ",");
1011 write!(output, "SpacetimeDB.Unit _Reserved");
1012 }
1013 _ => {}
1014 }
1015 }
1016 writeln!(output);
1017 writeln!(output, ")>;");
1018
1019 output.into_inner()
1020}
1021
1022fn autogen_csharp_plain_enum(enum_type_name: String, enum_type: &PlainEnumTypeDef, namespace: &str) -> String {
1023 let mut output = CsharpAutogen::new(namespace, &[]);
1024
1025 writeln!(output, "[SpacetimeDB.Type]");
1026 writeln!(output, "public enum {enum_type_name}");
1027 indented_block(&mut output, |output| {
1028 for variant in &*enum_type.variants {
1029 writeln!(output, "{variant},");
1030 }
1031 });
1032
1033 output.into_inner()
1034}
1035
1036fn autogen_csharp_tuple(module: &ModuleDef, name: String, tuple: &ProductTypeDef, namespace: &str) -> String {
1037 let mut output = CsharpAutogen::new(
1038 namespace,
1039 &["System.Collections.Generic", "System.Runtime.Serialization"],
1040 );
1041
1042 autogen_csharp_product_common(module, &mut output, name, tuple, "", |_| {});
1043
1044 output.into_inner()
1045}
1046
1047fn autogen_csharp_product_common(
1048 module: &ModuleDef,
1049 output: &mut CodeIndenter<String>,
1050 name: String,
1051 product_type: &ProductTypeDef,
1052 base: &str,
1053 extra_body: impl FnOnce(&mut CodeIndenter<String>),
1054) {
1055 writeln!(output, "[SpacetimeDB.Type]");
1056 writeln!(output, "[DataContract]");
1057 write!(output, "public sealed partial class {name}");
1058 if !base.is_empty() {
1059 write!(output, " : {base}");
1060 }
1061 writeln!(output);
1062 indented_block(output, |output| {
1063 let fields = product_type
1064 .into_iter()
1065 .map(|(orig_name, ty)| {
1066 writeln!(output, "[DataMember(Name = \"{orig_name}\")]");
1067
1068 let field_name = orig_name.deref().to_case(Case::Pascal);
1069 let ty = ty_fmt(module, ty).to_string();
1070
1071 writeln!(output, "public {ty} {field_name};");
1072
1073 (field_name, ty)
1074 })
1075 .collect::<Vec<_>>();
1076
1077 if !fields.is_empty() {
1079 writeln!(output);
1080
1081 write!(output, "public {name}(");
1083 if fields.len() > 1 {
1084 writeln!(output);
1085 }
1086 {
1087 indent_scope!(output);
1088 for (i, (field_name, ty)) in fields.iter().enumerate() {
1089 if i != 0 {
1090 writeln!(output, ",");
1091 }
1092 write!(output, "{ty} {field_name}");
1093 }
1094 }
1095 if fields.len() > 1 {
1096 writeln!(output);
1097 }
1098 writeln!(output, ")");
1099 indented_block(output, |output| {
1100 for (field_name, _ty) in fields.iter() {
1101 writeln!(output, "this.{field_name} = {field_name};");
1102 }
1103 });
1104 writeln!(output);
1105
1106 writeln!(output, "public {name}()");
1108 indented_block(output, |output| {
1109 for ((field_name, _ty), (_field, field_ty)) in fields.iter().zip(product_type) {
1110 if let Some(default) = default_init(module.typespace_for_generate(), field_ty) {
1111 writeln!(output, "this.{field_name} = {default};");
1112 }
1113 }
1114 });
1115 }
1116
1117 extra_body(output);
1118 });
1119}
1120
1121fn indented_block<R>(output: &mut CodeIndenter<String>, f: impl FnOnce(&mut CodeIndenter<String>) -> R) -> R {
1122 writeln!(output, "{{");
1123 let res = f(&mut output.indented(1));
1124 writeln!(output, "}}");
1125 res
1126}