nmusset opened a new issue, #288:
URL: https://github.com/apache/pulsar-dotpulsar/issues/288

   ### Description
   
   DotPulsar is not compatible with .NET Native AOT publishing. Attempting to 
use it in a project built with `<PublishAot>true</PublishAot>` produces 
multiple trim and dynamic-code warnings at build time, and the produced binary 
may crash at runtime when compression or schema features are exercised.
   
   The root causes fall into three areas:
   
   1. **Compression plugins** — all five compression backends 
(`BuiltinZlibCompression`, `Lz4Compression`, `SnappyCompression`, 
`ZstdCompression`, `ZstdSharpCompression`) discover and invoke the compression 
libraries entirely through `Assembly.Load()` + runtime reflection. These 
patterns are fundamentally incompatible with AOT and trimming.
   2. **Avro schemas** — `AvroISpecificRecordSchema<T>` and 
`AvroGenericRecordSchema` also use `Assembly.Load("Avro")`, `DefinedTypes`, 
`MakeGenericType()`, and `Activator.CreateInstance()` to bind to the Apache 
Avro library at runtime.
   3. **JSON schema** — `JsonSchemaDefinitionGenerator` inspects types via 
`GetProperties()` / `GetCustomAttribute()` / `Enum.GetNames()` at runtime, and 
`JsonSchema<T>` calls `JsonSerializer.Deserialize<T>` / `SerializeToUtf8Bytes` 
without a source-generated `JsonSerializerContext`, which is trim-unsafe.
   
   The library also declares no AOT-compatibility stance (`<IsAotCompatible>` 
is absent from the `.csproj`) and carries no `[RequiresDynamicCode]` / 
`[RequiresUnreferencedCode]` annotations to alert consumers.
   
   ### Reproduction Steps
   
   
   1. Create a new .NET 8/9/10 console application.
   2. Reference DotPulsar (latest, 5.3.1 at time of writing).
   3. Add `<PublishAot>true</PublishAot>` to the project file.
   4. Call any API that touches compression or a JSON/Avro schema (or simply 
add `using DotPulsar;`).
   5. Publish: `dotnet publish -r linux-x64 -c Release`
   
   Build output (verified with `dotnet publish -r win-x64 -c Release` against 
DotPulsar 5.3.1 / .NET 9 SDK) includes **39 warnings** — a representative 
sample:
   
   ```
   AvroISpecificRecordSchema.cs(114): Trim analysis warning IL2026: 
...Assembly.DefinedTypes.get which has 'RequiresUnreferencedCodeAttribute'...
   AvroISpecificRecordSchema.cs(136): AOT analysis warning IL3050: 
...System.Type.MakeGenericType(Type[]) which has 
'RequiresDynamicCodeAttribute'...
   AvroISpecificRecordSchema.cs(136): Trim analysis warning IL2055: Call to 
'System.Type.MakeGenericType(Type[])' can not be statically analyzed...
   BuiltinZlibCompression.cs(28):  Trim analysis warning IL2026: 
...Assembly.GetTypes() which has 'RequiresUnreferencedCodeAttribute'...
   BuiltinZlibCompression.cs(35):  AOT analysis warning IL3050: 
...System.Type.GetEnumValues() which has 'RequiresDynamicCodeAttribute'...
   Lz4Compression.cs(34):         Trim analysis warning IL2026: 
...Assembly.DefinedTypes.get which has 'RequiresUnreferencedCodeAttribute'...
   SnappyCompression.cs(32):       Trim analysis warning IL2026: 
...Assembly.DefinedTypes.get which has 'RequiresUnreferencedCodeAttribute'...
   ZstdCompression.cs(33):         Trim analysis warning IL2026: 
...Assembly.DefinedTypes.get which has 'RequiresUnreferencedCodeAttribute'...
   ZstdSharpCompression.cs(33):    Trim analysis warning IL2026: 
...Assembly.DefinedTypes.get which has 'RequiresUnreferencedCodeAttribute'...
   JsonSchemaDefinitionGenerator.cs(53): Trim analysis warning IL2070: 
...'this' argument does not satisfy 'PublicProperties' in call to 
'System.Type.GetProperties(BindingFlags)'...
   ```
   
   The full list is in the **Other information** section below.
   
   At runtime (depending on trimming aggressiveness), the compressor factory 
initialization will throw `FileNotFoundException` (trimmed assembly) or 
`MissingMethodException`.
   
   ### Expected behavior
   
   The library should either:
   - Be fully AOT-compatible for the core messaging path (at minimum) and 
annotate any optional features (Avro, dynamic compression loading) with 
`[RequiresDynamicCode]` / `[RequiresUnreferencedCode]` so the compiler can warn 
consumers; **or**
   - Declare `<IsAotCompatible>false</IsAotCompatible>` in the `.csproj` and 
document the limitation.
   
   Ideally the core path (connect, produce, consume with `byte[]` / primitive 
schemas) would be AOT-safe, with opt-in AOT-unsafe features clearly annotated.
   
   ### Actual behavior
   
   All five compression loaders use `Assembly.Load()` and extensive reflection 
with no annotations. The Avro schema implementations do the same. The JSON 
schema implementation uses un-annotated `System.Text.Json` reflection APIs. The 
project carries zero `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]` 
attributes and no `<IsAotCompatible>` marker.
   
   ### Regression?
   
   Not a regression — AOT compatibility has not been declared or targeted in 
any prior release.
   
   ### Known Workarounds
   
   - Build without `<PublishAot>` and instead use `<PublishSingleFile>` with 
`<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>` (dynamic 
loading still works).
   - Suppress individual warnings with `<NoWarn>` / `<TrimmerRootDescriptor>` 
in the consuming project (fragile, requires intimate knowledge of the library 
internals).
   - For the JSON schema specifically, pass a pre-built `jsonSchemaDefinition` 
string to `JsonSchema<T>(options, jsonSchemaDefinition)` and supply a 
source-generated `JsonSerializerContext` via `JsonSerializerOptions`.
   
   ### Configuration
   
   - DotPulsar version: 5.3.1
   - .NET SDK: 9.0 / 10.0 (both affected)
   - OS: Windows 11, Linux x64 (both affected)
   - Architecture: x64, ARM64
   
   ### Other information
   
   ### Detailed breakdown of AOT-incompatible code
   
   #### 1. Compression — `Assembly.Load()` + runtime reflection
   
   All five files under `src/DotPulsar/Internal/Compression/` follow the same 
pattern: load an optional assembly at runtime, enumerate its types by full 
name, and create delegates or instances via `Activator.CreateInstance` / 
`MethodInfo.CreateDelegate`. None of these patterns survive trimming.
   
   | File | Key problematic APIs |
   |------|----------------------|
   | `BuiltinZlibCompression.cs:27` | `Assembly.Load("System.IO.Compression")`, 
`assembly.GetTypes()`, `Activator.CreateInstance(zlibStreamType!, ...)` (lines 
60, 71) |
   | `Lz4Compression.cs:32` | `Assembly.Load("K4os.Compression.LZ4")`, 
`assembly.DefinedTypes.ToArray()`, `lz4Codec.GetMethods(BindingFlags.Public \| 
BindingFlags.Static)`, `method.CreateDelegate(...)` |
   | `SnappyCompression.cs:30` | `Assembly.Load("IronSnappy")`, 
`snappy.GetMethods(BindingFlags.Public \| BindingFlags.Static)`, 
`method.CreateDelegate(...)` |
   | `ZstdCompression.cs:31` | `Assembly.Load("ZstdNet")`, 
`GetMethods(BindingFlags.Public \| BindingFlags.Instance)`, 
`Activator.CreateInstance(...)`, `wrapMethod.CreateDelegate(...)` |
   | `ZstdSharpCompression.cs:31` | `Assembly.Load("ZstdSharp")`, same pattern 
as above |
   
   **Suggested fix:** Take compile-time dependencies on the optional 
compression packages instead of loading them dynamically. Gate each 
implementation behind a `#if` / multi-targeting or expose a registration API 
(`builder.UseSnappy()`) that accepts a concrete `ICompressorFactory` / 
`IDecompressorFactory` supplied by the consumer. This is the pattern used by 
libraries like `Microsoft.Extensions.Caching.StackExchangeRedis`.
   
   #### 2. Avro schemas — `Assembly.Load()` + `MakeGenericType` + 
`Activator.CreateInstance`
   
   `AvroISpecificRecordSchema<T>` (lines 54, 57, 59, 71–72, 113–114, 119–120, 
136, 149, 156, 159) and `AvroGenericRecordSchema` use an identical late-binding 
strategy against the `Apache.Avro` NuGet package.
   
   Notable calls:
   - `Assembly.Load("Avro")` — the assembly is trimmed away unless explicitly 
preserved
   - `assembly.DefinedTypes.ToArray()` — all types must survive trimming for 
this to work
   - `type.MakeGenericType(typeof(T))` — `[RequiresDynamicCode]`
   - `Activator.CreateInstance(_avroWriterTypeInfo, _avroSchema)` — 
`[RequiresUnreferencedCode]`
   - `MethodInfo.Invoke(...)` in `Decode` / `Encode` hot paths — defeats 
inlining and is trim-unsafe
   
   **Suggested fix:** Take a direct compile-time reference to `Apache.Avro` and 
use the concrete `SpecificDatumWriter<T>` / `SpecificDatumReader<T>` types. If 
the dependency must remain optional, annotate both schema classes with 
`[RequiresDynamicCode("Avro schema binding uses runtime reflection.")]` and 
`[RequiresUnreferencedCode(...)]`.
   
   #### 3. `JsonSchemaDefinitionGenerator` — runtime type inspection
   
   `src/DotPulsar/Schemas/JsonSchemaDefinitionGenerator.cs` calls 
`GetProperties(BindingFlags.Public | BindingFlags.Instance)` (line 53), 
`GetCustomAttribute<JsonIgnoreAttribute>()` (line 58), 
`GetCustomAttribute<JsonPropertyNameAttribute>()` (line 81), 
`Enum.GetNames(type)` (line 135), `type.GetElementType()` (line 154), 
`type.GetGenericTypeDefinition()` (line 161), and `type.GetGenericArguments()` 
(lines 170, 178) — all trim-unsafe when the concrete `T` is not statically 
known.
   
   **Suggested fix:** Replace with a `System.Text.Json` source-generated schema 
approach (available in .NET 9 via `JsonSchemaExporter` in `System.Text.Json`) 
or accept an explicit schema string and deprecate the reflection-based 
generator. Short-term: annotate `Generate(Type, ...)` with 
`[RequiresUnreferencedCode]`.
   
   #### 4. `JsonSchema<T>` — trim-unsafe `JsonSerializer` calls
   
   `src/DotPulsar/Schemas/JsonSchema.cs` calls 
`JsonSerializer.Deserialize<T>(array, _options)` (line 59) and 
`JsonSerializer.SerializeToUtf8Bytes(message, _options)` (line 70). Both 
overloads without a `JsonTypeInfo<T>` parameter are decorated with 
`[RequiresUnreferencedCode]` in the BCL.
   
   **Suggested fix:** Add an overload that accepts `JsonTypeInfo<T>` (or a 
`JsonSerializerContext`) and route serialization through it. Mark the existing 
reflection-based constructors with `[RequiresUnreferencedCode]`.
   
   #### 5. No `<IsAotCompatible>` declaration
   
   The absence of `<IsAotCompatible>true</IsAotCompatible>` in 
`DotPulsar.csproj` means the .NET SDK trim analyzer is **not** run during 
normal builds of the library itself. Adding 
`<IsAotCompatible>true</IsAotCompatible>` (even as a goal/aspiration) would 
surface all of the above as build-time errors in the library's own CI, making 
it impossible to accidentally re-introduce these patterns.
   
   ---
   
   ### Full verified warning list
   
   Obtained by running `dotnet publish -r win-x64 -c Release` against a minimal 
consumer project referencing DotPulsar 5.3.1 source (the native link step fails 
because MSVC is not on `PATH`, but all ILLink/AOT analyzer warnings are emitted 
before that step).
   
   **39 warnings across 8 source files:**
   
   | # | File | Line | Warning | Description |
   |---|------|------|---------|-------------|
   | 1 | `Schemas/AvroISpecificRecordSchema.cs` | 54 | IL2080 | 
`GetInterfaces()` — field `_typeT` not annotated with 
`DynamicallyAccessedMemberTypes.Interfaces` |
   | 2 | `Schemas/AvroISpecificRecordSchema.cs` | 57 | IL2080 | `GetField()` — 
field `_typeT` not annotated with `PublicFields` |
   | 3 | `Schemas/AvroISpecificRecordSchema.cs` | 71 | IL2075 | `GetProperty()` 
— return of `GetType()` not annotated with `PublicProperties` |
   | 4 | `Schemas/AvroISpecificRecordSchema.cs` | 72 | IL2075 | `GetMethod()` — 
return of `GetType()` not annotated with `PublicMethods` |
   | 5 | `Schemas/AvroISpecificRecordSchema.cs` | 114 | IL2026 | 
`Assembly.DefinedTypes` has `[RequiresUnreferencedCode]` — types may be removed 
|
   | 6 | `Schemas/AvroISpecificRecordSchema.cs` | 119 | IL2065 | `GetMethods()` 
— `this` value not statically determinable |
   | 7 | `Schemas/AvroISpecificRecordSchema.cs` | 120 | IL2065 | `GetMethods()` 
— same as above (second call) |
   | 8 | `Schemas/AvroISpecificRecordSchema.cs` | 136 | IL3050 | 
`MakeGenericType()` has `[RequiresDynamicCode]` — native code for instantiation 
may be absent |
   | 9 | `Schemas/AvroISpecificRecordSchema.cs` | 136 | IL2055 | 
`MakeGenericType()` — call cannot be statically analyzed |
   | 10 | `Schemas/AvroISpecificRecordSchema.cs` | 149 | IL3050 | 
`MakeGenericType()` — same (reader type) |
   | 11 | `Schemas/AvroISpecificRecordSchema.cs` | 149 | IL2055 | 
`MakeGenericType()` — same (reader type) |
   | 12 | `Schemas/AvroISpecificRecordSchema.cs` | 156 | IL2077 | 
`Activator.CreateInstance()` — field `_avroWriterTypeInfo` not annotated with 
`PublicConstructors` |
   | 13 | `Schemas/AvroISpecificRecordSchema.cs` | 159 | IL2077 | 
`Activator.CreateInstance()` — field `_avroReaderTypeInfo` not annotated with 
`PublicConstructors` |
   | 14 | `Internal/Compression/BuiltinZlibCompression.cs` | 28 | IL2026 | 
`Assembly.GetTypes()` has `[RequiresUnreferencedCode]` |
   | 15 | `Internal/Compression/BuiltinZlibCompression.cs` | 35 | IL3050 | 
`Type.GetEnumValues()` has `[RequiresDynamicCode]` |
   | 16 | `Internal/Compression/BuiltinZlibCompression.cs` | 37 | IL3050 | 
`Type.GetEnumValues()` — same (second enum type) |
   | 17 | `Internal/Compression/BuiltinZlibCompression.cs` | 60 | IL2067 | 
`Activator.CreateInstance()` — parameter `zlibStreamType` not annotated with 
`PublicConstructors` |
   | 18 | `Internal/Compression/BuiltinZlibCompression.cs` | 71 | IL2067 | 
`Activator.CreateInstance()` — same (decompressor lambda) |
   | 19 | `Internal/Compression/Lz4Compression.cs` | 34 | IL2026 | 
`Assembly.DefinedTypes` has `[RequiresUnreferencedCode]` |
   | 20 | `Internal/Compression/Lz4Compression.cs` | 39 | IL2075 | 
`GetMethods()` — return of `FindLZ4Codec()` not annotated with `PublicMethods` |
   | 21 | `Internal/Compression/SnappyCompression.cs` | 32 | IL2026 | 
`Assembly.DefinedTypes` has `[RequiresUnreferencedCode]` |
   | 22 | `Internal/Compression/SnappyCompression.cs` | 36 | IL2075 | 
`GetMethods()` — return of `FindSnappy()` not annotated with `PublicMethods` |
   | 23 | `Internal/Compression/ZstdCompression.cs` | 33 | IL2026 | 
`Assembly.DefinedTypes` has `[RequiresUnreferencedCode]` |
   | 24 | `Internal/Compression/ZstdCompression.cs` | 36 | IL2075 | 
`GetMethods()` — return of `Find()` not annotated with `PublicMethods` |
   | 25 | `Internal/Compression/ZstdCompression.cs` | 40 | IL2075 | 
`GetMethods()` — return of `Find()` not annotated with `PublicMethods` (second 
call) |
   | 26 | `Internal/Compression/ZstdCompression.cs` | 45 | IL2072 | 
`Activator.CreateInstance()` — return of `Find()` not annotated with 
`PublicParameterlessConstructor` |
   | 27 | `Internal/Compression/ZstdCompression.cs` | 55 | IL2072 | 
`Activator.CreateInstance()` — same (decompressor lambda) |
   | 28 | `Internal/Compression/ZstdCompression.cs` | 83 | IL2075 | 
`TypeInfo.ImplementedInterfaces` — enumerator current not annotated with 
`Interfaces` |
   | 29 | `Internal/Compression/ZstdCompression.cs` | 83 | IL2075 | 
`Type.GetConstructor()` — enumerator current not annotated with 
`PublicParameterlessConstructor` |
   | 30 | `Internal/Compression/ZstdSharpCompression.cs` | 33 | IL2026 | 
`Assembly.DefinedTypes` has `[RequiresUnreferencedCode]` |
   | 31 | `Internal/Compression/ZstdSharpCompression.cs` | 36 | IL2075 | 
`GetMethods()` — return of `FindDecompressor()` not annotated with 
`PublicMethods` |
   | 32 | `Internal/Compression/ZstdSharpCompression.cs` | 40 | IL2075 | 
`GetMethods()` — return of `FindCompressor()` not annotated with 
`PublicMethods` |
   | 33 | `Internal/Compression/ZstdSharpCompression.cs` | 45 | IL2072 | 
`Activator.CreateInstance()` — return of `FindCompressor()` not annotated with 
`PublicConstructors` |
   | 34 | `Internal/Compression/ZstdSharpCompression.cs` | 55 | IL2072 | 
`Activator.CreateInstance()` — return of `FindDecompressor()` not annotated 
with `PublicParameterlessConstructor` |
   | 35 | `Internal/Compression/ZstdSharpCompression.cs` | 83 | IL2075 | 
`TypeInfo.ImplementedInterfaces` — enumerator current not annotated with 
`Interfaces` (FindDecompressor) |
   | 36 | `Internal/Compression/ZstdSharpCompression.cs` | 83 | IL2075 | 
`Type.GetConstructor()` — enumerator current not annotated with 
`PublicParameterlessConstructor` (FindDecompressor) |
   | 37 | `Internal/Compression/ZstdSharpCompression.cs` | 103 | IL2075 | 
`TypeInfo.ImplementedInterfaces` — enumerator current not annotated with 
`Interfaces` (FindCompressor) |
   | 38 | `Internal/Compression/ZstdSharpCompression.cs` | 103 | IL2075 | 
`Type.GetConstructor()` — enumerator current not annotated with 
`PublicConstructors` (FindCompressor) |
   | 39 | `Schemas/JsonSchemaDefinitionGenerator.cs` | 53 | IL2070 | 
`GetProperties(BindingFlags)` — parameter `type` not annotated with 
`PublicProperties` |
   
   > **Note:** `AvroGenericRecordSchema` and the `JsonSchema<T>.Decode` / 
`Encode` paths were not reachable from the probe's entry point, so their 
warnings do not appear in this run. The actual warning count in a real 
application touching those code paths will be higher.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to