-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Description
Over the past year, I have reported a few issues (#22132, #22798, #24303) regarding the JavaScript proxy generation system. The proxy generator is a great feature that frees us from painfully generating frontend proxy code. ABP has made many excellent design choices in building this system, but unfortunately, it's still not correctly handling the nullability of DTO properties. I apologize for spamming the issues tracker with tickets of this nature, but I have a strong instinct that these problems belong to one systematic flaw in the design, and I hope this issue can address them altogether, once and for all.
With that in mind, I have put together the following truth table:
| C# | Example Type | T? | T | required T? | required T |
|---|---|---|---|---|---|
| Built-in Value | int |
✅ number? |
❌ number |
✅ number? |
❌ number |
| Special Struct | System.Guid |
✅ string? |
✅ string? |
✅ string? |
❌ string |
| Custom Struct¹ | struct S {} |
✅ S? |
❌ S |
✅ S? |
❌ S |
| CLR Enum² | System.DayOfWeek |
✅ any? |
✅ any? |
✅ any? |
❌ any |
| Custom Enum | enum E {} |
✅ E? |
✅ E? |
✅ E? |
❌ E |
| Special Class | string |
✅ string? |
✅ string? |
❌ string |
❌ string |
| Custom Class | class C {} |
❌ C |
❌ C |
❌ C |
❌ C |
| Array | int[] |
❌ number[] |
❌ number[] |
❌ number[] |
❌ number[] |
| Dictionary | Dictionary<string, string> |
❌ Record<string, string> |
❌ Record³ |
❌ Record³ |
❌ Record³ |
- Custom structs are generated such as
interface S extends any, which does not work.- Not all CLR enums are tested.
- Shortened for conciseness.
This table shows how the nullability of a C# type is handled in the generated TypeScript code. For example, int? number?, int number, required int? number?, required int number. For clarity of reading, I added a ✅ for nullable type and ❌ for non-nullable type generated.
From this table, it is difficult to get a clear impression of how nullability works in this system. In real-world usage, it brings a lot of surprises. For instance, your C# DTO might say an int[] property is optional, but on the TypeScript side, you are forced to provide a value. If you see an optional enum property, naturally omit it, and pass that DTO to your C# backend, you might not realize that on the C# side it's actually non-nullable, resulting in a deserialization exception. For a C# required string?, you are not allowed to pass a null value on the TypeScript side. In a strictly regulated codebase, we are forced to fix all these nullability issues every time we regenerate the proxy, which makes the proxy generator much less of a pleasure to use than it should be.
Base on the discussion in this issue, it seems ABP's intention was to use the required keyword to determine nullability. However:
- We can see this rule is not consistent across different types. While all
required Tare generated as non-nullable,T?,T, andrequired T?all behave differently. It seems the rule - if there is one clearly defined - is very convoluted. - We can argue that
requiredis semantically not suitable for expressing whether a property should be nullable.required T?is totally valid: this property must be specified, but it can be set tonull.
On the other hand, it's far more natural to use .NET's nullability annotation for this job, which brings the rules down to a nice one-liner: If the property is nullable in C# (type name followed by a question mark), it is generated as nullable.
I did not explore the technical possibilities, and totally understand that there might be difficulty to retrieve the nullability information in the runtime, so I'm definitely open to ideas.