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