Skip to content

Enable IsAotCompatible#96

Open
campersau wants to merge 10 commits intotestcontainers:mainfrom
campersau:aot
Open

Enable IsAotCompatible#96
campersau wants to merge 10 commits intotestcontainers:mainfrom
campersau:aot

Conversation

@campersau
Copy link
Copy Markdown

@campersau campersau commented Mar 30, 2026

  • Enable IsAotCompatible
  • Generate DockerModelsJsonSerializerContext with all docker API models which have JSON parameters, so query string only models are not added for JsonSerialization
  • Add DockerExtendedJsonSerializerContext which includes types directly used by *Operations (this can be removed / merged with DockerModelsJsonSerializerContext when STJ source generator fails when defining the serializer context in 2 partial classes dotnet/runtime#99669 is available)
  • Update reflection APIs with DynamicallyAccessedMembers attributes
  • Update xunit to xunit.v3.aot.mtp-v2 4.0.0-pre.33 which is still in preview but required to actually test AOT.
  • Use xunit.v3.mtp-v2 4.0.0-pre.33 for .NET 8.0 because xunit AOT only works from .NET 9 onwards.
  • Cleanup preprocessor directives
  • Change all IEnumerable responses to be IList for consistency (breaking change)
  • Use generic QueryStringParameter to reduce reflection

fixes #88

@campersau
Copy link
Copy Markdown
Author

campersau commented Apr 1, 2026

Before (main)

Method Mean Error StdDev Allocated
CreateContainerRequestResponse 190.7 ms 107.9 ms 5.91 ms 34.55 KB
StartContainerRequestResponse 717.5 ms 371.3 ms 20.35 ms 47.59 KB

Docker.DotNet.dll 464 KB (475.648 Bytes)

image

After (AoT)

Method Mean Error StdDev Allocated
CreateContainerRequestResponse 185.8 ms 109.1 ms 5.98 ms 41.77 KB
StartContainerRequestResponse 702.5 ms 478.7 ms 26.24 ms 68.9 KB

Docker.DotNet.dll 3,01 MB (3.162.112 Bytes)

image

@HofmeisterAn
Copy link
Copy Markdown
Collaborator

FYI: I will need a few days for the review. I'm pretty busy this week.

@HofmeisterAn
Copy link
Copy Markdown
Collaborator

I'll do the review this weekend.

@campersau
Copy link
Copy Markdown
Author

No worries. This currently uses a preview version of xunit aot. Stable release should be soon.

Copy link
Copy Markdown
Collaborator

@HofmeisterAn HofmeisterAn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the PR looks good! I just have a few questions so I can understand a few things better.

Comment thread test/Docker.DotNet.Tests/TestFixture.cs Outdated
Comment thread tools/specgen/specgen.go
Comment thread src/Docker.DotNet/Endpoints/ISwarmOperations.cs
Comment thread src/Docker.DotNet/JsonSerializer.cs Outdated
} No newline at end of file
}

// extended types that are not generated by source generator
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do you think we can generate these? What about a separate C# generator or validation tool for the API 😬?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am currently not sure what the best way is to automate this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, difficult... What do you think about a test that uses reflection to check whether the return and parameter types of the API (interface) implementations are part of the context?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we can write an analyser. Never done that from scratch but I can try. We need to now the generic type of MakeRequestAsync.
Not sure if a source generator would work because STJ also uses a source generator.

Copy link
Copy Markdown
Collaborator

@HofmeisterAn HofmeisterAn Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to now the generic type of MakeRequestAsync.

Yea, that's right. My idea e.g. fails with (PluginInstallParameters vs. IList<PluginPrivilege>):

public Task InstallPluginAsync(PluginInstallParameters parameters, IProgress<JSONMessage> progress, CancellationToken cancellationToken = default)
{
if (parameters == null)
{
throw new ArgumentNullException(nameof(parameters));
}
if (parameters.Privileges == null)
{
throw new ArgumentNullException(nameof(parameters.Privileges));
}
var queryParameters = new QueryString<PluginInstallParameters>(parameters);
var data = new JsonRequestContent<IList<PluginPrivilege>>(parameters.Privileges, DockerClient.JsonSerializer);
return StreamUtil.MonitorStreamForMessagesAsync(
_client.MakeRequestForStreamAsync(_client.NoErrorHandlers, HttpMethod.Post, $"plugins/pull", queryParameters, data, null, cancellationToken),
progress,
cancellationToken);
}

Probably we need:

  • MakeRequestAsync
  • JsonRequestContent
  • MonitorStreamForMessagesAsync

But we can also handle that in a follow-up PR if that's more convenient.

Comment thread src/Docker.DotNet/QueryStringParameterAttribute.cs Outdated
@@ -15,11 +15,15 @@ static JsonSerializer()

private JsonSerializer()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following is slightly better, although the improvements compared to network I/O are probably negligible. WDYT?

Subject: [PATCH] f
---
Index: src/Docker.DotNet/JsonSerializer.cs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs
--- a/src/Docker.DotNet/JsonSerializer.cs	(revision e4cc73fdd8371f3dd6110684f6e7a705b55941c4)
+++ b/src/Docker.DotNet/JsonSerializer.cs	(date 1776437559579)
@@ -42,26 +42,27 @@
 
     public string Serialize<T>(T value)
     {
-        return System.Text.Json.JsonSerializer.Serialize(value, (JsonTypeInfo<T>)_options.GetTypeInfo(typeof(T)));
+        return System.Text.Json.JsonSerializer.Serialize(value, TypeInfoCache<T>.Value);
     }
 
     public byte[] SerializeToUtf8Bytes<T>(T value)
     {
-        return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, (JsonTypeInfo<T>)_options.GetTypeInfo(typeof(T)));
+        return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, TypeInfoCache<T>.Value);
     }
 
     public T Deserialize<T>(byte[] json)
     {
-        return System.Text.Json.JsonSerializer.Deserialize(json, (JsonTypeInfo<T>)_options.GetTypeInfo(typeof(T)))!;
+        return System.Text.Json.JsonSerializer.Deserialize(json, TypeInfoCache<T>.Value)!;
     }
 
     public Task<T> DeserializeAsync<T>(HttpContent content, CancellationToken cancellationToken)
     {
-        return content.ReadFromJsonAsync((JsonTypeInfo<T>)_options.GetTypeInfo(typeof(T)), cancellationToken)!;
+        return content.ReadFromJsonAsync(TypeInfoCache<T>.Value, cancellationToken)!;
     }
 
     public async IAsyncEnumerable<T> DeserializeAsync<T>(Stream stream, [EnumeratorCancellation] CancellationToken cancellationToken)
     {
+        var typeInfo = TypeInfoCache<T>.Value;
         var reader = PipeReader.Create(stream);
 
         while (true)
@@ -73,7 +74,7 @@
 
             while (!buffer.IsEmpty && TryParseJson(ref buffer, out var jsonDocument))
             {
-                yield return jsonDocument!.Deserialize((JsonTypeInfo<T>)_options.GetTypeInfo(typeof(T)))!;
+                yield return jsonDocument!.Deserialize(typeInfo)!;
             }
 
             if (result.IsCompleted)
@@ -99,6 +100,11 @@
 
         return false;
     }
+
+    private static class TypeInfoCache<T>
+    {
+        public static readonly JsonTypeInfo<T> Value = (JsonTypeInfo<T>)Instance._options.GetTypeInfo(typeof(T));
+    }
 }
 
 // Additional source-generated metadata for collections and dictionaries used by operations.

// PluginOperations.ListPluginsAsync
[JsonSerializable(typeof(Plugin[]))]
// PluginOperations.GetPrivilegesAsync
[JsonSerializable(typeof(PluginPrivilege[]))]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one more topic, but aside from the open discussions, the PR looks good. We can also address some of those in a follow-up PR if you prefer.

PluginPrivilege[] covers the array response, but InstallPluginAsync and UpgradePluginAsync serialize an IList<PluginPrivilege>.

IList<PluginPrivilege> is a different closed type from PluginPrivilege[], so it needs its own JsonSerializable entry in the combined serializer context, right?

I belive the analyzer is a good idea 💡.

I think these are missing too (or we change them to arrays too):

// PluginOperations.InstallPluginAsync, UpgradePluginAsync
[JsonSerializable(typeof(IList<PluginPrivilege>))]
// PluginOperations.ConfigurePluginAsync
[JsonSerializable(typeof(IList<string>))]

// SecretsOperations.ListAsync
[JsonSerializable(typeof(IList<Secret>))]

// TasksOperations.ListAsync
[JsonSerializable(typeof(IList<TaskResponse>))]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the library compatible with Native AOT

2 participants