spacetimedb_cli/subcommands/generate/
csharp.rs

1// Note: the generated code depends on APIs and interfaces from crates/bindings-csharp/BSATN.Runtime.
2use 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            // We can't guarantee that the output lives inside a valid project or solution,
427            // so to avoid crash we need to use the `dotnet whitespace --folder` mode instead
428            // of a full style-aware formatter. Still better than nothing though.
429            "whitespace",
430            "--folder",
431            // Our files are marked with <auto-generated /> and will be skipped without this option.
432            "--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                // If this is a table, we want to generate event accessor and indexes
476                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                        // If there is no accessor name, we shouldn't generate a client-side index accessor.
485                        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                // Write out arguments one per line
645                {
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            // Prevent instantiation of this class from outside.
734            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        // Options (`T?`) have a default value of null which is fine for us.
877        AlgebraicTypeUse::Option(_) => None,
878        AlgebraicTypeUse::Ref(r) => match &ctx[*r] {
879            // TODO: generate some proper default here (what would it be for tagged enums?).
880            AlgebraicTypeDef::Sum(_) => Some("null!"),
881            // Simple enums have their own default (variant with value of zero).
882            AlgebraicTypeDef::PlainEnum(_) => None,
883            AlgebraicTypeDef::Product(_) => Some("new()"),
884        },
885        // See Sum(_) handling above.
886        AlgebraicTypeUse::ScheduleAt => Some("null!"),
887        AlgebraicTypeUse::Array(_) => Some("new()"),
888        // Strings must have explicit default value of "".
889        AlgebraicTypeUse::String => Some(r#""""#),
890        // Primitives are initialized to zero automatically.
891        AlgebraicTypeUse::Primitive(_) => None,
892        // these are structs, they are initialized to zero-filled automatically
893        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        // Don't emit `using SpacetimeDB;` if we are going to be nested in the SpacetimeDB namespace.
931        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        // If we have fewer than 2 variants, we need to add some dummy variants to make the tuple work.
977        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 we don't have any fields, the default constructor is fine, otherwise we need to generate our own.
1052        if !fields.is_empty() {
1053            writeln!(output);
1054
1055            // Generate fully-parameterized constructor.
1056            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            // Generate default constructor.
1081            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}