spacetimedb_codegen/
csharp.rs

1// Note: the generated code depends on APIs and interfaces from crates/bindings-csharp/BSATN.Runtime.
2use 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                // If this is a table, we want to generate event accessor and indexes
482                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                        // If there is no accessor name, we shouldn't generate a client-side index accessor.
491                        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                // Write out arguments one per line
663                {
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            // Prevent instantiation of this class from outside.
756            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        // Options (`T?`) have a default value of null which is fine for us.
903        AlgebraicTypeUse::Option(_) => None,
904        AlgebraicTypeUse::Ref(r) => match &ctx[*r] {
905            // TODO: generate some proper default here (what would it be for tagged enums?).
906            AlgebraicTypeDef::Sum(_) => Some("null!"),
907            // Simple enums have their own default (variant with value of zero).
908            AlgebraicTypeDef::PlainEnum(_) => None,
909            AlgebraicTypeDef::Product(_) => Some("new()"),
910        },
911        // See Sum(_) handling above.
912        AlgebraicTypeUse::ScheduleAt => Some("null!"),
913        AlgebraicTypeUse::Array(_) => Some("new()"),
914        // Strings must have explicit default value of "".
915        AlgebraicTypeUse::String => Some(r#""""#),
916        // Primitives are initialized to zero automatically.
917        AlgebraicTypeUse::Primitive(_) => None,
918        // these are structs, they are initialized to zero-filled automatically
919        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        // Don't emit `using SpacetimeDB;` if we are going to be nested in the SpacetimeDB namespace.
957        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        // If we have fewer than 2 variants, we need to add some dummy variants to make the tuple work.
1003        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 we don't have any fields, the default constructor is fine, otherwise we need to generate our own.
1078        if !fields.is_empty() {
1079            writeln!(output);
1080
1081            // Generate fully-parameterized constructor.
1082            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            // Generate default constructor.
1107            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}