From 956b5e672be3afc80910c7d650c5786107bc773b Mon Sep 17 00:00:00 2001 From: campersau Date: Mon, 23 Mar 2026 20:43:57 +0100 Subject: [PATCH 01/24] Enable IsAotCompatible --- .github/workflows/ci.yml | 10 +- Directory.Packages.props | 5 +- global.json | 5 + src/Directory.Build.props | 1 + .../DockerHandlerFactory.cs | 6 +- .../DockerHandlerFactory.cs | 2 +- .../NativeHttpTransportOptions.cs | 2 +- .../CertificateCredentials.cs | 2 +- .../DockerTlsCertificates.cs | 8 +- src/Docker.DotNet/Docker.DotNet.csproj | 1 + src/Docker.DotNet/DockerClient.cs | 2 +- .../IQueryStringConverterInstanceFactory.cs | 6 +- src/Docker.DotNet/JsonSerializer.cs | 27 +- ...erModelsJsonSerializerContext.Generated.cs | 300 ++++++++++++++++++ src/Docker.DotNet/QueryString.cs | 15 +- .../QueryStringConverterInstanceFactory.cs | 26 +- .../QueryStringParameterAttribute.cs | 11 +- .../HttpConnection.cs | 4 +- .../HttpConnectionResponseContent.cs | 2 +- .../RequestExtensions.cs | 4 +- .../Docker.DotNet.Tests.csproj | 24 +- .../IConfigOperationsTests.cs | 12 +- .../IContainerOperationsTests.cs | 30 +- .../IImageOperationsTests.cs | 5 +- .../ISwarmOperationsTests.cs | 44 +-- .../ISystemOperations.Tests.cs | 26 +- .../JsonEnumMemberConverterTest.cs | 2 +- .../JsonRequestContentTests.cs | 6 +- test/Docker.DotNet.Tests/TestFixture.cs | 4 +- .../Docker.DotNet.TestsV2.csproj | 24 +- tools/specgen/specgen.go | 31 ++ 31 files changed, 519 insertions(+), 128 deletions(-) create mode 100644 global.json create mode 100644 src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0559e652..90d97e1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,13 +132,9 @@ jobs: exit 1 - name: Test (${{ matrix.docker.name }}) - run: >- - dotnet test - --configuration Release - --framework ${{ matrix.dotnet.tfm }} - --no-restore - --no-build - --logger console + run: | + ./test/Docker.DotNet.Tests/bin/Release/${{ matrix.dotnet.tfm }}/linux-x64/publish/Docker.DotNet.Tests + ./test/Docker.DotNet.TestsV2/bin/Release/${{ matrix.dotnet.tfm }}/linux-x64/publish/Docker.DotNet.TestsV2 env: DOCKER_HOST: ${{ matrix.docker.docker_host }} DOCKER_TLS_VERIFY: ${{ matrix.docker.tls_verify }} diff --git a/Directory.Packages.props b/Directory.Packages.props index c8be7ea0..3bb23f85 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,8 +11,7 @@ - - - + + diff --git a/global.json b/global.json new file mode 100644 index 00000000..802ab217 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index efc57594..a6367c51 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,6 +20,7 @@ enable + true diff --git a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs index 40d003f4..1310e069 100644 --- a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs @@ -45,10 +45,10 @@ public ResolvedTransport CreateHandler(NPipeTransportOptions transportOptions, C var dockerStream = new DockerPipeStream(clientStream); -#if NETSTANDARD - var namedPipeConnectTimeout = (int)transportOptions.ConnectTimeout.TotalMilliseconds; -#else +#if NET var namedPipeConnectTimeout = transportOptions.ConnectTimeout; +#else + var namedPipeConnectTimeout = (int)transportOptions.ConnectTimeout.TotalMilliseconds; #endif await clientStream.ConnectAsync(namedPipeConnectTimeout, cancellationToken) diff --git a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs index 4ee2d2d1..a7c568f8 100644 --- a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs @@ -29,7 +29,7 @@ public ResolvedTransport CreateHandler(NativeHttpTransportOptions transportOptio var scheme = clientOptions.AuthProvider.TlsEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; var uri = new UriBuilder(clientOptions.Endpoint) { Scheme = scheme }.Uri; -#if NET6_0_OR_GREATER +#if NET var handler = new SocketsHttpHandler { MaxConnectionsPerServer = MaxConnectionsPerServer, diff --git a/src/Docker.DotNet.NativeHttp/NativeHttpTransportOptions.cs b/src/Docker.DotNet.NativeHttp/NativeHttpTransportOptions.cs index 5bf408ce..45a295fa 100644 --- a/src/Docker.DotNet.NativeHttp/NativeHttpTransportOptions.cs +++ b/src/Docker.DotNet.NativeHttp/NativeHttpTransportOptions.cs @@ -5,7 +5,7 @@ namespace Docker.DotNet.NativeHttp; /// public sealed record NativeHttpTransportOptions { -#if NET6_0_OR_GREATER +#if NET /// /// Gets a callback that configures the created instance. /// diff --git a/src/Docker.DotNet.X509/CertificateCredentials.cs b/src/Docker.DotNet.X509/CertificateCredentials.cs index 1c732bbd..2b37fe04 100644 --- a/src/Docker.DotNet.X509/CertificateCredentials.cs +++ b/src/Docker.DotNet.X509/CertificateCredentials.cs @@ -15,7 +15,7 @@ public CertificateCredentials(X509Certificate2? certificate) public HttpMessageHandler ConfigureHandler(HttpMessageHandler handler) { -#if NET6_0_OR_GREATER +#if NET if (handler is SocketsHttpHandler socketsHandler) { if (_certificate != null) diff --git a/src/Docker.DotNet.X509/DockerTlsCertificates.cs b/src/Docker.DotNet.X509/DockerTlsCertificates.cs index 193fb85c..bbf58750 100644 --- a/src/Docker.DotNet.X509/DockerTlsCertificates.cs +++ b/src/Docker.DotNet.X509/DockerTlsCertificates.cs @@ -64,7 +64,7 @@ public static X509Certificate2 LoadCertificateFromPemFiles(string certPemPath, s } return certificate; -#elif NET6_0_OR_GREATER +#elif NET var certificate = X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); if (OperatingSystem.IsWindows()) @@ -75,10 +75,8 @@ public static X509Certificate2 LoadCertificateFromPemFiles(string certPemPath, s } return certificate; -#elif NETSTANDARD - return Polyfills.X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); #else - return X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); + return Polyfills.X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); #endif } @@ -141,7 +139,7 @@ public static RemoteCertificateValidationCallback CreateCertificateAuthorityVali using var chain = new X509Chain(); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; -#if NET5_0_OR_GREATER +#if NET chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; chain.ChainPolicy.CustomTrustStore.Add(certificateAuthorityCertificate); return chain.Build(serverCertificate2); diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index da5c71ad..5ce06281 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 6754f9b6..d9f1b1b5 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -370,7 +370,7 @@ private async Task PrivateMakeRequestAsync( if (Timeout.InfiniteTimeSpan == timeout) { -#if NET6_0_OR_GREATER +#if NET return await _client.SendAsync(request, completionOption, cancellationToken) .ConfigureAwait(false); #else diff --git a/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs b/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs index cd81a9c5..a8770c15 100644 --- a/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs +++ b/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs @@ -2,5 +2,9 @@ namespace Docker.DotNet; internal interface IQueryStringConverterInstanceFactory { - IQueryStringConverter GetConverterInstance(Type t); + IQueryStringConverter GetConverterInstance( +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + Type t); } \ No newline at end of file diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 87e11f9b..34d5a0c3 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization.Metadata; + namespace Docker.DotNet; internal sealed class JsonSerializer @@ -15,11 +17,16 @@ static JsonSerializer() private JsonSerializer() { + _options.TypeInfoResolver = JsonTypeInfoResolver.Combine( + DockerModelsJsonSerializerContext.Default, + DockerExtendedJsonSerializerContext.Default + ); _options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; _options.Converters.Add(new JsonEnumMemberConverter()); _options.Converters.Add(new JsonEnumMemberConverter()); _options.Converters.Add(new JsonDateTimeConverter()); _options.Converters.Add(new JsonNullableDateTimeConverter()); + _options.MakeReadOnly(); } public static JsonSerializer Instance { get; } @@ -38,22 +45,22 @@ public HttpContent GetHttpContent(T value) public string Serialize(T value) { - return System.Text.Json.JsonSerializer.Serialize(value, _options); + return System.Text.Json.JsonSerializer.Serialize(value, (JsonTypeInfo)_options.GetTypeInfo(typeof(T))); } public byte[] SerializeToUtf8Bytes(T value) { - return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, _options); + return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, (JsonTypeInfo)_options.GetTypeInfo(typeof(T))); } public T Deserialize(byte[] json) { - return System.Text.Json.JsonSerializer.Deserialize(json, _options)!; + return System.Text.Json.JsonSerializer.Deserialize(json, (JsonTypeInfo)_options.GetTypeInfo(typeof(T)))!; } public Task DeserializeAsync(HttpContent content, CancellationToken cancellationToken) { - return content.ReadFromJsonAsync(_options, cancellationToken)!; + return content.ReadFromJsonAsync((JsonTypeInfo)_options.GetTypeInfo(typeof(T)), cancellationToken)!; } public async IAsyncEnumerable DeserializeAsync(Stream stream, [EnumeratorCancellation] CancellationToken cancellationToken) @@ -69,7 +76,7 @@ public async IAsyncEnumerable DeserializeAsync(Stream stream, [EnumeratorC while (!buffer.IsEmpty && TryParseJson(ref buffer, out var jsonDocument)) { - yield return jsonDocument!.Deserialize(_options)!; + yield return jsonDocument!.Deserialize((JsonTypeInfo)_options.GetTypeInfo(typeof(T)))!; } if (result.IsCompleted) @@ -95,4 +102,12 @@ private static bool TryParseJson(ref ReadOnlySequence buffer, out JsonDocu return false; } -} \ No newline at end of file +} + +// extended types that are not generated by source generator +[JsonSerializable(typeof(Dictionary[]))] +[JsonSerializable(typeof(ImagesListResponse[]))] +[JsonSerializable(typeof(ContainerListResponse[]))] +[JsonSerializable(typeof(SwarmService[]))] +[JsonSerializable(typeof(IList))] +internal sealed partial class DockerExtendedJsonSerializerContext : JsonSerializerContext { } diff --git a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs new file mode 100644 index 00000000..4f316fc5 --- /dev/null +++ b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs @@ -0,0 +1,300 @@ +namespace Docker.DotNet.Models +{ + [JsonSerializable(typeof(Actor))] + [JsonSerializable(typeof(Annotations))] + [JsonSerializable(typeof(AppArmorOpts))] + [JsonSerializable(typeof(AttestationProperties))] + [JsonSerializable(typeof(AuthConfig))] + [JsonSerializable(typeof(AuthResponse))] + [JsonSerializable(typeof(BindOptions))] + [JsonSerializable(typeof(BlkioStatEntry))] + [JsonSerializable(typeof(BlkioStats))] + [JsonSerializable(typeof(CAConfig))] + [JsonSerializable(typeof(CPUStats))] + [JsonSerializable(typeof(CPUUsage))] + [JsonSerializable(typeof(CapacityRange))] + [JsonSerializable(typeof(ClusterInfo))] + [JsonSerializable(typeof(ClusterOptions))] + [JsonSerializable(typeof(ClusterVolume))] + [JsonSerializable(typeof(ClusterVolumeSpec))] + [JsonSerializable(typeof(Commit))] + [JsonSerializable(typeof(CommitContainerChangesParameters))] + [JsonSerializable(typeof(CommitContainerChangesResponse))] + [JsonSerializable(typeof(ComponentVersion))] + [JsonSerializable(typeof(ConfigReference))] + [JsonSerializable(typeof(ConfigReferenceFileTarget))] + [JsonSerializable(typeof(ConfigReferenceRuntimeTarget))] + [JsonSerializable(typeof(ConsoleSize))] + [JsonSerializable(typeof(ContainerAttachParameters))] + [JsonSerializable(typeof(ContainerConfig))] + [JsonSerializable(typeof(ContainerEventsParameters))] + [JsonSerializable(typeof(ContainerExecCreateParameters))] + [JsonSerializable(typeof(ContainerExecCreateResponse))] + [JsonSerializable(typeof(ContainerExecInspectResponse))] + [JsonSerializable(typeof(ContainerExecStartParameters))] + [JsonSerializable(typeof(ContainerFileSystemChangeResponse))] + [JsonSerializable(typeof(ContainerInspectParameters))] + [JsonSerializable(typeof(ContainerInspectResponse))] + [JsonSerializable(typeof(ContainerKillParameters))] + [JsonSerializable(typeof(ContainerListProcessesParameters))] + [JsonSerializable(typeof(ContainerListResponse))] + [JsonSerializable(typeof(ContainerLogsParameters))] + [JsonSerializable(typeof(ContainerPathStatParameters))] + [JsonSerializable(typeof(ContainerPathStatResponse))] + [JsonSerializable(typeof(ContainerProcessesResponse))] + [JsonSerializable(typeof(ContainerRemoveParameters))] + [JsonSerializable(typeof(ContainerRenameParameters))] + [JsonSerializable(typeof(ContainerResizeParameters))] + [JsonSerializable(typeof(ContainerRestartParameters))] + [JsonSerializable(typeof(ContainerSpec))] + [JsonSerializable(typeof(ContainerStartParameters))] + [JsonSerializable(typeof(ContainerStatsParameters))] + [JsonSerializable(typeof(ContainerStatsResponse))] + [JsonSerializable(typeof(ContainerStatus))] + [JsonSerializable(typeof(ContainerStopParameters))] + [JsonSerializable(typeof(ContainerUpdateParameters))] + [JsonSerializable(typeof(ContainerUpdateResponse))] + [JsonSerializable(typeof(ContainerWaitResponse))] + [JsonSerializable(typeof(ContainerdInfo))] + [JsonSerializable(typeof(ContainerdNamespaces))] + [JsonSerializable(typeof(ContainersListParameters))] + [JsonSerializable(typeof(ContainersPruneParameters))] + [JsonSerializable(typeof(ContainersPruneResponse))] + [JsonSerializable(typeof(CopyToContainerParameters))] + [JsonSerializable(typeof(CreateContainerParameters))] + [JsonSerializable(typeof(CreateContainerResponse))] + [JsonSerializable(typeof(CredentialSpec))] + [JsonSerializable(typeof(DNSConfig))] + [JsonSerializable(typeof(Descriptor))] + [JsonSerializable(typeof(DeviceInfo))] + [JsonSerializable(typeof(DeviceMapping))] + [JsonSerializable(typeof(DeviceRequest))] + [JsonSerializable(typeof(DiscreteGenericResource))] + [JsonSerializable(typeof(DispatcherConfig))] + [JsonSerializable(typeof(DockerOCIImageConfig))] + [JsonSerializable(typeof(DockerOCIImageConfigExt))] + [JsonSerializable(typeof(Driver))] + [JsonSerializable(typeof(DriverData))] + [JsonSerializable(typeof(EncryptionConfig))] + [JsonSerializable(typeof(Endpoint))] + [JsonSerializable(typeof(EndpointIPAMConfig))] + [JsonSerializable(typeof(EndpointResource))] + [JsonSerializable(typeof(EndpointSettings))] + [JsonSerializable(typeof(EndpointSpec))] + [JsonSerializable(typeof(EndpointVirtualIP))] + [JsonSerializable(typeof(EngineDescription))] + [JsonSerializable(typeof(ExecProcessConfig))] + [JsonSerializable(typeof(ExternalCA))] + [JsonSerializable(typeof(FirewallInfo))] + [JsonSerializable(typeof(GenericResource))] + [JsonSerializable(typeof(GlobalJob))] + [JsonSerializable(typeof(GlobalService))] + [JsonSerializable(typeof(Health))] + [JsonSerializable(typeof(HealthSummary))] + [JsonSerializable(typeof(HealthcheckConfig))] + [JsonSerializable(typeof(HealthcheckResult))] + [JsonSerializable(typeof(HostConfig))] + [JsonSerializable(typeof(IPAM))] + [JsonSerializable(typeof(IPAMConfig))] + [JsonSerializable(typeof(IPAMOptions))] + [JsonSerializable(typeof(IPAMStatus))] + [JsonSerializable(typeof(ImageBuildParameters))] + [JsonSerializable(typeof(ImageBuildResult))] + [JsonSerializable(typeof(ImageConfig))] + [JsonSerializable(typeof(ImageDeleteParameters))] + [JsonSerializable(typeof(ImageDeleteResponse))] + [JsonSerializable(typeof(ImageHistoryResponse))] + [JsonSerializable(typeof(ImageInspectResponse))] + [JsonSerializable(typeof(ImageLoadParameters))] + [JsonSerializable(typeof(ImageOptions))] + [JsonSerializable(typeof(ImageProperties))] + [JsonSerializable(typeof(ImagePropertiesSize))] + [JsonSerializable(typeof(ImagePushParameters))] + [JsonSerializable(typeof(ImageSearchResponse))] + [JsonSerializable(typeof(ImageTagParameters))] + [JsonSerializable(typeof(ImagesCreateParameters))] + [JsonSerializable(typeof(ImagesListParameters))] + [JsonSerializable(typeof(ImagesListResponse))] + [JsonSerializable(typeof(ImagesLoadResponse))] + [JsonSerializable(typeof(ImagesPruneParameters))] + [JsonSerializable(typeof(ImagesPruneResponse))] + [JsonSerializable(typeof(ImagesSearchParameters))] + [JsonSerializable(typeof(IndexInfo))] + [JsonSerializable(typeof(Info))] + [JsonSerializable(typeof(JSONError))] + [JsonSerializable(typeof(JSONMessage))] + [JsonSerializable(typeof(JSONProgress))] + [JsonSerializable(typeof(JobStatus))] + [JsonSerializable(typeof(JoinTokens))] + [JsonSerializable(typeof(LogConfig))] + [JsonSerializable(typeof(ManagerStatus))] + [JsonSerializable(typeof(ManifestSummary))] + [JsonSerializable(typeof(ManifestSummarySize))] + [JsonSerializable(typeof(MemoryStats))] + [JsonSerializable(typeof(Message))] + [JsonSerializable(typeof(Meta))] + [JsonSerializable(typeof(Metadata))] + [JsonSerializable(typeof(Mount))] + [JsonSerializable(typeof(MountPoint))] + [JsonSerializable(typeof(NamedGenericResource))] + [JsonSerializable(typeof(Network))] + [JsonSerializable(typeof(NetworkAddressPool))] + [JsonSerializable(typeof(NetworkAttachment))] + [JsonSerializable(typeof(NetworkAttachmentConfig))] + [JsonSerializable(typeof(NetworkAttachmentSpec))] + [JsonSerializable(typeof(NetworkConnectParameters))] + [JsonSerializable(typeof(NetworkDisconnectParameters))] + [JsonSerializable(typeof(NetworkResponse))] + [JsonSerializable(typeof(NetworkSettings))] + [JsonSerializable(typeof(NetworkSettingsSummary))] + [JsonSerializable(typeof(NetworkSpec))] + [JsonSerializable(typeof(NetworkStats))] + [JsonSerializable(typeof(NetworkTask))] + [JsonSerializable(typeof(NetworkingConfig))] + [JsonSerializable(typeof(NetworksCreateParameters))] + [JsonSerializable(typeof(NetworksCreateResponse))] + [JsonSerializable(typeof(NetworksDeleteUnusedParameters))] + [JsonSerializable(typeof(NetworksListParameters))] + [JsonSerializable(typeof(NetworksPruneResponse))] + [JsonSerializable(typeof(NodeCSIInfo))] + [JsonSerializable(typeof(NodeDescription))] + [JsonSerializable(typeof(NodeListResponse))] + [JsonSerializable(typeof(NodeRemoveParameters))] + [JsonSerializable(typeof(NodeStatus))] + [JsonSerializable(typeof(NodeUpdateParameters))] + [JsonSerializable(typeof(OrchestrationConfig))] + [JsonSerializable(typeof(Peer))] + [JsonSerializable(typeof(PeerInfo))] + [JsonSerializable(typeof(PidsStats))] + [JsonSerializable(typeof(Placement))] + [JsonSerializable(typeof(PlacementPreference))] + [JsonSerializable(typeof(Platform))] + [JsonSerializable(typeof(PlatformInfo))] + [JsonSerializable(typeof(Plugin))] + [JsonSerializable(typeof(PluginArgs))] + [JsonSerializable(typeof(PluginCapabilityID))] + [JsonSerializable(typeof(PluginConfig))] + [JsonSerializable(typeof(PluginConfigureParameters))] + [JsonSerializable(typeof(PluginCreateParameters))] + [JsonSerializable(typeof(PluginDescription))] + [JsonSerializable(typeof(PluginDevice))] + [JsonSerializable(typeof(PluginDisableParameters))] + [JsonSerializable(typeof(PluginEnableParameters))] + [JsonSerializable(typeof(PluginEnv))] + [JsonSerializable(typeof(PluginGetPrivilegeParameters))] + [JsonSerializable(typeof(PluginInstallParameters))] + [JsonSerializable(typeof(PluginInterface))] + [JsonSerializable(typeof(PluginLinuxConfig))] + [JsonSerializable(typeof(PluginListParameters))] + [JsonSerializable(typeof(PluginMount))] + [JsonSerializable(typeof(PluginNetworkConfig))] + [JsonSerializable(typeof(PluginPrivilege))] + [JsonSerializable(typeof(PluginRemoveParameters))] + [JsonSerializable(typeof(PluginRootFS))] + [JsonSerializable(typeof(PluginSettings))] + [JsonSerializable(typeof(PluginUpgradeParameters))] + [JsonSerializable(typeof(PluginUser))] + [JsonSerializable(typeof(PluginsInfo))] + [JsonSerializable(typeof(PortBinding))] + [JsonSerializable(typeof(PortConfig))] + [JsonSerializable(typeof(PortStatus))] + [JsonSerializable(typeof(PortSummary))] + [JsonSerializable(typeof(Privileges))] + [JsonSerializable(typeof(PublishStatus))] + [JsonSerializable(typeof(RaftConfig))] + [JsonSerializable(typeof(ReplicatedJob))] + [JsonSerializable(typeof(ReplicatedService))] + [JsonSerializable(typeof(ResourceRequirements))] + [JsonSerializable(typeof(Resources))] + [JsonSerializable(typeof(RestartPolicy))] + [JsonSerializable(typeof(RootFS))] + [JsonSerializable(typeof(RootFSStorage))] + [JsonSerializable(typeof(RootFSStorageSnapshot))] + [JsonSerializable(typeof(Runtime))] + [JsonSerializable(typeof(RuntimePrivilege))] + [JsonSerializable(typeof(RuntimeWithStatus))] + [JsonSerializable(typeof(SELinuxContext))] + [JsonSerializable(typeof(SeccompOpts))] + [JsonSerializable(typeof(Secret))] + [JsonSerializable(typeof(SecretCreateResponse))] + [JsonSerializable(typeof(SecretReference))] + [JsonSerializable(typeof(SecretReferenceFileTarget))] + [JsonSerializable(typeof(ServiceConfig))] + [JsonSerializable(typeof(ServiceCreateParameters))] + [JsonSerializable(typeof(ServiceCreateResponse))] + [JsonSerializable(typeof(ServiceInfo))] + [JsonSerializable(typeof(ServiceListParameters))] + [JsonSerializable(typeof(ServiceLogsParameters))] + [JsonSerializable(typeof(ServiceMode))] + [JsonSerializable(typeof(ServiceSpec))] + [JsonSerializable(typeof(ServiceStatus))] + [JsonSerializable(typeof(ServiceUpdateParameters))] + [JsonSerializable(typeof(ServiceUpdateResponse))] + [JsonSerializable(typeof(Spec))] + [JsonSerializable(typeof(SpreadOver))] + [JsonSerializable(typeof(State))] + [JsonSerializable(typeof(Status))] + [JsonSerializable(typeof(Storage))] + [JsonSerializable(typeof(StorageStats))] + [JsonSerializable(typeof(SubnetStatus))] + [JsonSerializable(typeof(SummaryHostConfig))] + [JsonSerializable(typeof(SwarmConfig))] + [JsonSerializable(typeof(SwarmConfigReference))] + [JsonSerializable(typeof(SwarmConfigSpec))] + [JsonSerializable(typeof(SwarmCreateConfigParameters))] + [JsonSerializable(typeof(SwarmCreateConfigResponse))] + [JsonSerializable(typeof(SwarmDriver))] + [JsonSerializable(typeof(SwarmIPAMConfig))] + [JsonSerializable(typeof(SwarmInitParameters))] + [JsonSerializable(typeof(SwarmInspectResponse))] + [JsonSerializable(typeof(SwarmJoinParameters))] + [JsonSerializable(typeof(SwarmLeaveParameters))] + [JsonSerializable(typeof(SwarmLimit))] + [JsonSerializable(typeof(SwarmNetwork))] + [JsonSerializable(typeof(SwarmPlatform))] + [JsonSerializable(typeof(SwarmResources))] + [JsonSerializable(typeof(SwarmRestartPolicy))] + [JsonSerializable(typeof(SwarmRuntimeSpec))] + [JsonSerializable(typeof(SwarmSecretSpec))] + [JsonSerializable(typeof(SwarmService))] + [JsonSerializable(typeof(SwarmUnlockParameters))] + [JsonSerializable(typeof(SwarmUnlockResponse))] + [JsonSerializable(typeof(SwarmUpdateConfig))] + [JsonSerializable(typeof(SwarmUpdateConfigParameters))] + [JsonSerializable(typeof(SwarmUpdateParameters))] + [JsonSerializable(typeof(SystemInfoResponse))] + [JsonSerializable(typeof(TLSInfo))] + [JsonSerializable(typeof(TaskDefaults))] + [JsonSerializable(typeof(TaskResponse))] + [JsonSerializable(typeof(TaskSpec))] + [JsonSerializable(typeof(TaskStatus))] + [JsonSerializable(typeof(TasksListParameters))] + [JsonSerializable(typeof(ThrottleDevice))] + [JsonSerializable(typeof(ThrottlingData))] + [JsonSerializable(typeof(TmpfsOptions))] + [JsonSerializable(typeof(Topology))] + [JsonSerializable(typeof(TopologyRequirement))] + [JsonSerializable(typeof(TypeBlock))] + [JsonSerializable(typeof(TypeMount))] + [JsonSerializable(typeof(Ulimit))] + [JsonSerializable(typeof(UpdateConfig))] + [JsonSerializable(typeof(UpdateStatus))] + [JsonSerializable(typeof(UsageData))] + [JsonSerializable(typeof(Version))] + [JsonSerializable(typeof(VersionResponse))] + [JsonSerializable(typeof(VolumeAccessMode))] + [JsonSerializable(typeof(VolumeAttachment))] + [JsonSerializable(typeof(VolumeInfo))] + [JsonSerializable(typeof(VolumeOptions))] + [JsonSerializable(typeof(VolumeResponse))] + [JsonSerializable(typeof(VolumeSecret))] + [JsonSerializable(typeof(VolumeTopology))] + [JsonSerializable(typeof(VolumesCreateParameters))] + [JsonSerializable(typeof(VolumesListParameters))] + [JsonSerializable(typeof(VolumesListResponse))] + [JsonSerializable(typeof(VolumesPruneParameters))] + [JsonSerializable(typeof(VolumesPruneResponse))] + [JsonSerializable(typeof(WaitExitError))] + [JsonSerializable(typeof(WeightDevice))] + internal sealed partial class DockerModelsJsonSerializerContext : JsonSerializerContext { } +} diff --git a/src/Docker.DotNet/QueryString.cs b/src/Docker.DotNet/QueryString.cs index e12869e5..89f68134 100644 --- a/src/Docker.DotNet/QueryString.cs +++ b/src/Docker.DotNet/QueryString.cs @@ -1,6 +1,10 @@ namespace Docker.DotNet; -internal class QueryString : IQueryString where T : class +internal class QueryString< +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif +T> : IQueryString where T : class { private T Object { get; } @@ -86,7 +90,11 @@ private static string[] ConvertValue(IQueryStringConverter converter, object val return converter.Convert(value); } - private static Dictionary FindAttributedPublicProperties() where TAttribType : Attribute + private static Dictionary FindAttributedPublicProperties< +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TValue, TAttribType>() where TAttribType : Attribute { Dictionary? attributedPublicProperties = null; @@ -113,6 +121,9 @@ private static Dictionary FindAttributedPublicPropert return attributedPublicProperties; } +#if NET + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Activator.CreateInstance is only used for value types here; safe for runtime usage.")] +#endif private static bool IsDefaultOfType(object? o) { if (o is ValueType) diff --git a/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs b/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs index 1450f50e..dba24b4e 100644 --- a/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs +++ b/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs @@ -4,20 +4,28 @@ internal class QueryStringConverterInstanceFactory : IQueryStringConverterInstan { private static readonly ConcurrentDictionary ConverterInstanceRegistry = new ConcurrentDictionary(); - public IQueryStringConverter GetConverterInstance(Type t) + public IQueryStringConverter GetConverterInstance( +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + Type t) { - return ConverterInstanceRegistry.GetOrAdd( - t, - InitializeConverter); +#pragma warning disable IL2111 // Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. + return ConverterInstanceRegistry.GetOrAdd(t, InitializeConverter); +#pragma warning restore IL2111 // Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. } - private IQueryStringConverter InitializeConverter(Type t) + private static IQueryStringConverter InitializeConverter( +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + Type t) { - var instance = Activator.CreateInstance(t) as IQueryStringConverter; - if (instance == null) + if (Activator.CreateInstance(t) is IQueryStringConverter instance) { - throw new InvalidOperationException($"Could not get instance of {t.FullName}"); + return instance; } - return instance; + + throw new InvalidOperationException($"Could not get instance of {t.FullName}"); } } \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringParameterAttribute.cs b/src/Docker.DotNet/QueryStringParameterAttribute.cs index 6b08d259..e088e39e 100644 --- a/src/Docker.DotNet/QueryStringParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringParameterAttribute.cs @@ -7,16 +7,23 @@ internal sealed class QueryStringParameterAttribute : Attribute public bool IsRequired { get; private set; } +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif public Type? ConverterType { get; private set; } - public QueryStringParameterAttribute(string name, bool required, Type? converterType = null) + public QueryStringParameterAttribute(string name, bool required, +#if NET + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + Type? converterType = null) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } - if (converterType != null && !converterType.GetInterfaces().Contains(typeof (IQueryStringConverter))) + if (converterType != null && !converterType.GetInterfaces().Contains(typeof(IQueryStringConverter))) { throw new ArgumentException($"Provided query string converter type is not '{typeof(IQueryStringConverter).FullName}'.", nameof(converterType)); } diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index fb8c1ab2..7cf88c49 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -26,7 +26,7 @@ await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationTok { if (request.Content.Headers.ContentLength.HasValue) { -#if NET6_0_OR_GREATER +#if NET await request.Content.CopyToAsync(Transport, cancellationToken) .ConfigureAwait(false); #else @@ -39,7 +39,7 @@ await request.Content.CopyToAsync(Transport) // The length of the data is unknown. Send it in chunked mode. using (var chunkedStream = new ChunkedWriteStream(Transport)) { -#if NET6_0_OR_GREATER +#if NET await request.Content.CopyToAsync(chunkedStream, cancellationToken) .ConfigureAwait(false); #else diff --git a/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs index 4fbaefc2..5bed410a 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -45,7 +45,7 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext? return _responseStream!.CopyToAsync(stream); } -#if NET6_0_OR_GREATER +#if NET protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) { return _responseStream!.CopyToAsync(stream, cancellationToken); diff --git a/src/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Microsoft.Net.Http.Client/RequestExtensions.cs index c7872e17..e2be5054 100644 --- a/src/Microsoft.Net.Http.Client/RequestExtensions.cs +++ b/src/Microsoft.Net.Http.Client/RequestExtensions.cs @@ -84,7 +84,7 @@ public static void SetAddressLineProperty(this HttpRequestMessage request, strin public static T? GetProperty(this HttpRequestMessage request, string key) { -#if NET6_0_OR_GREATER +#if NET return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; #else return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; @@ -93,7 +93,7 @@ public static void SetAddressLineProperty(this HttpRequestMessage request, strin public static void SetProperty(this HttpRequestMessage request, string key, T value) { -#if NET6_0_OR_GREATER +#if NET request.Options.Set(new HttpRequestOptionsKey(key), value); #else request.Properties[key] = value; diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 2d80e0e5..190d1748 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -1,13 +1,21 @@ - + net8.0;net9.0;net10.0 + linux-arm64 + linux-x64 + osx-arm64 + osx-x64 + win-arm64 + win-x64 + Exe false - false + true + true + true - - - + + @@ -24,7 +32,6 @@ - @@ -40,7 +47,10 @@ - + + + + diff --git a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs index 533d5d25..96bb49ac 100644 --- a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs @@ -15,7 +15,7 @@ public IConfigOperationsTests(TestFixture testFixture, ITestOutputHelper testOut [Fact] public async Task SwarmConfig_CanCreateAndRead() { - var currentConfigs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); + var currentConfigs = await _testFixture.DockerClient.Configs.ListConfigsAsync(TestContext.Current.CancellationToken); _testOutputHelper.WriteLine($"Current Configs: {currentConfigs.Count}"); @@ -31,15 +31,15 @@ public async Task SwarmConfig_CanCreateAndRead() Config = testConfigSpec }; - var createdConfig = await _testFixture.DockerClient.Configs.CreateConfigAsync(configParameters); + var createdConfig = await _testFixture.DockerClient.Configs.CreateConfigAsync(configParameters, TestContext.Current.CancellationToken); Assert.NotNull(createdConfig.ID); _testOutputHelper.WriteLine($"Config created: {createdConfig.ID}"); - var configs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); + var configs = await _testFixture.DockerClient.Configs.ListConfigsAsync(TestContext.Current.CancellationToken); Assert.Contains(configs, c => c.ID == createdConfig.ID); _testOutputHelper.WriteLine($"Current Configs: {configs.Count}"); - var configResponse = await _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID); + var configResponse = await _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID, TestContext.Current.CancellationToken); Assert.NotNull(configResponse); @@ -51,8 +51,8 @@ public async Task SwarmConfig_CanCreateAndRead() _testOutputHelper.WriteLine("Config created is the same."); - await _testFixture.DockerClient.Configs.RemoveConfigAsync(createdConfig.ID); + await _testFixture.DockerClient.Configs.RemoveConfigAsync(createdConfig.ID, TestContext.Current.CancellationToken); - await Assert.ThrowsAsync(() => _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID, TestContext.Current.CancellationToken)); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index ff3ebab9..c2e36892 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -94,7 +94,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( _testFixture.Cts.Token ); - await Task.Delay(TimeSpan.FromSeconds(5)); + await Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); await _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, @@ -141,7 +141,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( _testFixture.Cts.Token ); - await Task.Delay(TimeSpan.FromSeconds(5)); + await Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); await _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, @@ -280,7 +280,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.Token ); - await Task.Delay(TimeSpan.FromSeconds(5)); + await Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, @@ -328,7 +328,7 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( tcs.Token ); - await Task.Delay(TimeSpan.FromSeconds(10)); + await Task.Delay(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); @@ -341,9 +341,7 @@ public async Task GetContainerStatsAsync_Tty_False_StreamStats() using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_False_StreamStats"))) { - var method = MethodBase.GetCurrentMethod(); - - _testOutputHelper.WriteLine($"Running test '{method!.Module}' -> '{method!.Name}'"); + _testOutputHelper.WriteLine($"Running test GetContainerStatsAsync_Tty_False_StreamStats"); var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters @@ -421,7 +419,7 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( tcs.Token ); - await Task.Delay(TimeSpan.FromSeconds(10)); + await Task.Delay(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); @@ -475,7 +473,7 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( // This is expected to happen on task cancellation. } - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); _testOutputHelper.WriteLine($"Container stats count: {containerStatsList.Count}"); Assert.NotEmpty(containerStatsList); } @@ -727,10 +725,10 @@ public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_Comple containerAttachParameters.Stream = true; // When - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters, TestContext.Current.CancellationToken); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), TestContext.Current.CancellationToken); - using var stream = await _testFixture.DockerClient.Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters); + using var stream = await _testFixture.DockerClient.Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters, TestContext.Current.CancellationToken); await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); @@ -762,12 +760,12 @@ public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_Comple var containerExecStartParameters = new ContainerExecStartParameters(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters, TestContext.Current.CancellationToken); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), TestContext.Current.CancellationToken); // When - var containerExecCreateResponse = await _testFixture.DockerClient.Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters); - using var stream = await _testFixture.DockerClient.Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); + var containerExecCreateResponse = await _testFixture.DockerClient.Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters, TestContext.Current.CancellationToken); + using var stream = await _testFixture.DockerClient.Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters, TestContext.Current.CancellationToken); await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); diff --git a/test/Docker.DotNet.Tests/IImageOperationsTests.cs b/test/Docker.DotNet.Tests/IImageOperationsTests.cs index 50d522f7..9eed7749 100644 --- a/test/Docker.DotNet.Tests/IImageOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IImageOperationsTests.cs @@ -50,12 +50,11 @@ await _testFixture.DockerClient.Images.TagImageAsync( [Fact] public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException() { - return Assert.ThrowsAsync(() => _testFixture.DockerClient.Images.CreateImageAsync( - new ImagesCreateParameters + return Assert.ThrowsAsync(() => _testFixture.DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = "1.2.3.Apparently&this$is+not-a_valid%repository//name", Tag = "ancient-one" - }, null, null)); + }, null, null, TestContext.Current.CancellationToken)); } [Fact] diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index e620446c..9e86e2ed 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -24,7 +24,7 @@ public async Task GetFilteredServicesByName_Succeeds() Name = serviceName, TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { @@ -33,7 +33,7 @@ public async Task GetFilteredServicesByName_Succeeds() Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { @@ -42,7 +42,7 @@ public async Task GetFilteredServicesByName_Succeeds() Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters { @@ -57,9 +57,9 @@ public async Task GetFilteredServicesByName_Succeeds() Assert.Single(services); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId, TestContext.Current.CancellationToken); } [Fact] @@ -72,7 +72,7 @@ public async Task GetFilteredServicesById_Succeeds() Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { @@ -81,7 +81,7 @@ public async Task GetFilteredServicesById_Succeeds() Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { @@ -90,7 +90,7 @@ public async Task GetFilteredServicesById_Succeeds() Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters { @@ -105,9 +105,9 @@ public async Task GetFilteredServicesById_Succeeds() Assert.Single(services); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId, TestContext.Current.CancellationToken); } [Fact] @@ -122,7 +122,7 @@ public async Task GetServices_Succeeds() Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { @@ -131,7 +131,7 @@ public async Task GetServices_Succeeds() Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { @@ -140,15 +140,15 @@ public async Task GetServices_Succeeds() Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); Assert.True(services.Count() > initialServiceCount); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId, TestContext.Current.CancellationToken); } [Fact] @@ -165,14 +165,14 @@ public async Task GetServiceLogs_Succeeds() Name = serviceName, TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID, Command = CommonCommands.EchoToStdoutAndStderr } } } - })).ID; + }, TestContext.Current.CancellationToken)).ID; using var stream = await _testFixture.DockerClient.Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters { Follow = true, ShowStdout = true, ShowStderr = true - }); + }, TestContext.Current.CancellationToken); var maxRetries = 3; var currentRetry = 0; @@ -238,7 +238,7 @@ public async Task GetServiceLogs_Succeeds() currentRetry++; if (currentRetry < maxRetries) { - await Task.Delay(delayBetweenRetries); + await Task.Delay(delayBetweenRetries, TestContext.Current.CancellationToken); } } } @@ -246,6 +246,6 @@ public async Task GetServiceLogs_Succeeds() Assert.NotNull(logLines); Assert.NotEmpty(logLines); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(serviceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(serviceId, TestContext.Current.CancellationToken); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs index b94faa24..d1064779 100644 --- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs +++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs @@ -22,14 +22,14 @@ public void Docker_IsRunning() [Fact] public async Task GetSystemInfoAsync_Succeeds() { - var info = await _testFixture.DockerClient.System.GetSystemInfoAsync(); + var info = await _testFixture.DockerClient.System.GetSystemInfoAsync(TestContext.Current.CancellationToken); Assert.NotNull(info.Architecture); } [Fact] public async Task GetVersionAsync_Succeeds() { - var version = await _testFixture.DockerClient.System.GetVersionAsync(); + var version = await _testFixture.DockerClient.System.GetVersionAsync(TestContext.Current.CancellationToken); Assert.NotNull(version.APIVersion); } @@ -40,7 +40,7 @@ public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled() using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - await Task.Delay(1); + await Task.Delay(1, TestContext.Current.CancellationToken); await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); @@ -49,13 +49,13 @@ public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled() [Fact] public async Task MonitorEventsAsync_NullParameters_Throws() { - await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(null, null)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(null, null, TestContext.Current.CancellationToken)); } [Fact] public async Task MonitorEventsAsync_NullProgress_Throws() { - await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), null)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), null, TestContext.Current.CancellationToken)); } [Fact] @@ -90,7 +90,7 @@ await _testFixture.DockerClient.Images.DeleteImageAsync( _testFixture.Cts.Token); // Give it some time for output operation to complete before cancelling task - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); await cts.CancelAsync(); @@ -223,19 +223,19 @@ await _testFixture.DockerClient.Images.TagImageAsync( }); using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); - var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token)); + var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token), TestContext.Current.CancellationToken); // Wait briefly to ensure the monitoring task is fully established before triggering Docker events. // Ideally, the API would return (or signal) once monitoring is active. - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); - await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); - await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); + await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }, TestContext.Current.CancellationToken); + await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters(), TestContext.Current.CancellationToken); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }, TestContext.Current.CancellationToken); await _testFixture.DockerClient.Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); await cts.CancelAsync(); await Assert.ThrowsAnyAsync(() => task); @@ -247,6 +247,6 @@ await _testFixture.DockerClient.Images.TagImageAsync( [Fact] public async Task PingAsync_Succeeds() { - await _testFixture.DockerClient.System.PingAsync(); + await _testFixture.DockerClient.System.PingAsync(TestContext.Current.CancellationToken); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/JsonEnumMemberConverterTest.cs b/test/Docker.DotNet.Tests/JsonEnumMemberConverterTest.cs index ad26d82b..7a16d9d3 100644 --- a/test/Docker.DotNet.Tests/JsonEnumMemberConverterTest.cs +++ b/test/Docker.DotNet.Tests/JsonEnumMemberConverterTest.cs @@ -26,7 +26,7 @@ public void JsonSerialization_ShouldSerializeAndDeserializeCorrectly(RestartPoli Assert.Equal(restartPolicyKind, deserializedParameters.HostConfig.RestartPolicy.Name); } - private sealed class RestartPolicyKindTestData : TheoryData + public sealed class RestartPolicyKindTestData : TheoryData { public RestartPolicyKindTestData() { diff --git a/test/Docker.DotNet.Tests/JsonRequestContentTests.cs b/test/Docker.DotNet.Tests/JsonRequestContentTests.cs index 7731f612..ef61bb41 100644 --- a/test/Docker.DotNet.Tests/JsonRequestContentTests.cs +++ b/test/Docker.DotNet.Tests/JsonRequestContentTests.cs @@ -17,10 +17,10 @@ public void Constructor_ThrowsArgumentNullException_WhenSerializerIsNull() [Fact] public async Task GetContent_Succeeds_WhenValueAndSerializerAreValid() { - var content = new JsonRequestContent(new[] { 1 }, JsonSerializer.Instance); + var content = new JsonRequestContent[]>([new Dictionary() { { "key", "value" } }], JsonSerializer.Instance); using var httpContent = content.GetContent(); Assert.Equal("application/json; charset=utf-8", httpContent.Headers.ContentType.ToString()); - var jsonString = await httpContent.ReadAsStringAsync(); - Assert.Equal("[1]", jsonString); + var jsonString = await httpContent.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.Equal("""[{"key":"value"}]""", jsonString); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 9f3c7280..2c5c5205 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -54,7 +54,7 @@ public TestFixture(IMessageSink messageSink) public ImagesListResponse Image { get; private set; } /// - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { const string repository = "alpine"; @@ -102,7 +102,7 @@ await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImag } /// - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { if (_hasInitializedSwarm) { diff --git a/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj b/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj index 6f9298b7..388cc7cd 100644 --- a/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj +++ b/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj @@ -1,13 +1,21 @@ - + net8.0;net9.0;net10.0 + linux-arm64 + linux-x64 + osx-arm64 + osx-x64 + win-arm64 + win-x64 + Exe false - false + true + true + true - - - + + @@ -18,7 +26,6 @@ - @@ -29,7 +36,8 @@ - - + + + diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index 0672cda9..5b569e04 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -855,6 +855,8 @@ func main() { reflectType(t) } + names := make([]string, 0, len(reflectedTypes)) + for k, v := range reflectedTypes { if _, e := os.Stat(path.Join(sourcePath, v.Name+".Generated.cs")); e == nil { panic(fmt.Sprintf("File: (%s.Generated.cs) already exists. Failed to write key same name for key: (%s) type: (%s).", v.Name, k, v.SourceName)) @@ -877,5 +879,34 @@ func main() { f.Close() os.Rename(f.Name(), path.Join(sourcePath, v.Name+".Generated.cs")) + + names = append(names, v.Name) + } + + slices.Sort(names) + + jscf, err := os.Create(path.Join(sourcePath, "DockerModelsJsonSerializerContext.Generated.cs")) + if err != nil { + panic(err) + } + + defer jscf.Close() + + jscb := bufio.NewWriter(jscf) + + fmt.Fprintln(jscb, "namespace Docker.DotNet.Models") + fmt.Fprintln(jscb, "{") + for _, name := range names { + fmt.Fprintf(jscb, " [JsonSerializable(typeof(%s))]\n", name) } + fmt.Fprintln(jscb, " internal sealed partial class DockerModelsJsonSerializerContext : JsonSerializerContext { }") + fmt.Fprintln(jscb, "}") + + err = jscb.Flush() + if err != nil { + os.Remove(jscf.Name()) + panic(err) + } + + jscf.Close() } From 5911a04c4fcaf022712db793948b297a53b01ead Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 1 Apr 2026 00:10:33 +0200 Subject: [PATCH 02/24] reduce reflection and simplify querystring converter instance factory by using generic attributes --- .../IQueryStringConverterInstanceFactory.cs | 10 ------ ...mitContainerChangesParameters.Generated.cs | 4 +-- .../ContainerAttachParameters.Generated.cs | 10 +++--- .../ContainerEventsParameters.Generated.cs | 2 +- .../ContainerInspectParameters.Generated.cs | 2 +- .../ContainerLogsParameters.Generated.cs | 8 ++--- .../ContainerRemoveParameters.Generated.cs | 6 ++-- .../ContainerStatsParameters.Generated.cs | 4 +-- .../ContainersListParameters.Generated.cs | 6 ++-- .../ContainersPruneParameters.Generated.cs | 2 +- .../CopyToContainerParameters.Generated.cs | 4 +-- .../Models/ImageBuildParameters.Generated.cs | 20 ++++++------ .../Models/ImageDeleteParameters.Generated.cs | 4 +-- .../Models/ImageLoadParameters.Generated.cs | 2 +- .../ImagesCreateParameters.Generated.cs | 2 +- .../Models/ImagesListParameters.Generated.cs | 10 +++--- .../Models/ImagesPruneParameters.Generated.cs | 2 +- .../ImagesSearchParameters.Generated.cs | 2 +- ...etworksDeleteUnusedParameters.Generated.cs | 2 +- .../NetworksListParameters.Generated.cs | 2 +- .../Models/NodeRemoveParameters.Generated.cs | 2 +- .../PluginDisableParameters.Generated.cs | 2 +- .../Models/PluginListParameters.Generated.cs | 2 +- .../PluginRemoveParameters.Generated.cs | 2 +- .../Models/ServiceListParameters.Generated.cs | 4 +-- .../Models/ServiceLogsParameters.Generated.cs | 10 +++--- .../Models/SwarmLeaveParameters.Generated.cs | 2 +- .../Models/SwarmUpdateParameters.Generated.cs | 6 ++-- .../Models/TasksListParameters.Generated.cs | 2 +- .../Models/VolumesListParameters.Generated.cs | 2 +- .../VolumesPruneParameters.Generated.cs | 2 +- src/Docker.DotNet/QueryString.cs | 18 +++++------ .../QueryStringConverterInstanceFactory.cs | 31 ------------------- .../QueryStringParameterAttribute.cs | 26 ++++++---------- tools/specgen/specgen.go | 22 +++++++------ 35 files changed, 94 insertions(+), 143 deletions(-) delete mode 100644 src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs delete mode 100644 src/Docker.DotNet/QueryStringConverterInstanceFactory.cs diff --git a/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs b/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs deleted file mode 100644 index a8770c15..00000000 --- a/src/Docker.DotNet/IQueryStringConverterInstanceFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Docker.DotNet; - -internal interface IQueryStringConverterInstanceFactory -{ - IQueryStringConverter GetConverterInstance( -#if NET - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] -#endif - Type t); -} \ No newline at end of file diff --git a/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs b/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs index de1e14dc..1eb3e160 100644 --- a/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs +++ b/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs @@ -53,10 +53,10 @@ public CommitContainerChangesParameters(ContainerConfig Config) [QueryStringParameter("author", false)] public string? Author { get; set; } - [QueryStringParameter("changes", false, typeof(EnumerableQueryStringConverter))] + [QueryStringParameter("changes", false)] public IList? Changes { get; set; } - [QueryStringParameter("pause", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("pause", false)] public bool? Pause { get; set; } [JsonPropertyName("Hostname")] diff --git a/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs index 536058c3..f277ffba 100644 --- a/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs @@ -3,22 +3,22 @@ namespace Docker.DotNet.Models { public class ContainerAttachParameters // (main.ContainerAttachParameters) { - [QueryStringParameter("stream", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stream", false)] public bool? Stream { get; set; } - [QueryStringParameter("stdin", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stdin", false)] public bool? Stdin { get; set; } - [QueryStringParameter("stdout", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stdout", false)] public bool? Stdout { get; set; } - [QueryStringParameter("stderr", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stderr", false)] public bool? Stderr { get; set; } [QueryStringParameter("detachKeys", false)] public string? DetachKeys { get; set; } - [QueryStringParameter("logs", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("logs", false)] public bool? Logs { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs index 7214bcc7..728f69ab 100644 --- a/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs @@ -9,7 +9,7 @@ public class ContainerEventsParameters // (main.ContainerEventsParameters) [QueryStringParameter("until", false)] public string? Until { get; set; } - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs index 9da5c347..35cd3880 100644 --- a/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ContainerInspectParameters // (main.ContainerInspectParameters) { - [QueryStringParameter("size", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("size", false)] public bool? IncludeSize { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs index 32c8e832..653b9d82 100644 --- a/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ContainerLogsParameters // (main.ContainerLogsParameters) { - [QueryStringParameter("stdout", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stdout", false)] public bool? ShowStdout { get; set; } - [QueryStringParameter("stderr", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stderr", false)] public bool? ShowStderr { get; set; } [QueryStringParameter("since", false)] @@ -15,10 +15,10 @@ public class ContainerLogsParameters // (main.ContainerLogsParameters) [QueryStringParameter("until", false)] public string? Until { get; set; } - [QueryStringParameter("timestamps", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("timestamps", false)] public bool? Timestamps { get; set; } - [QueryStringParameter("follow", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("follow", false)] public bool? Follow { get; set; } [QueryStringParameter("tail", false)] diff --git a/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs index 861ab467..ce982bcc 100644 --- a/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs @@ -3,13 +3,13 @@ namespace Docker.DotNet.Models { public class ContainerRemoveParameters // (main.ContainerRemoveParameters) { - [QueryStringParameter("v", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("v", false)] public bool? RemoveVolumes { get; set; } - [QueryStringParameter("link", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("link", false)] public bool? RemoveLinks { get; set; } - [QueryStringParameter("force", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs index 8c00b12d..fb01ca98 100644 --- a/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ContainerStatsParameters // (main.ContainerStatsParameters) { - [QueryStringParameter("stream", true, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stream", true)] public bool Stream { get; set; } = true; - [QueryStringParameter("one-shot", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("one-shot", false)] public bool? OneShot { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs b/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs index f7573786..1781acc9 100644 --- a/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs @@ -3,16 +3,16 @@ namespace Docker.DotNet.Models { public class ContainersListParameters // (main.ContainersListParameters) { - [QueryStringParameter("all", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("all", false)] public bool? All { get; set; } [QueryStringParameter("limit", false)] public long? Limit { get; set; } - [QueryStringParameter("size", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("size", false)] public bool? Size { get; set; } - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs b/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs index e25de825..c619c2b6 100644 --- a/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ContainersPruneParameters // (main.ContainersPruneParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs b/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs index b3e0608c..af17cb52 100644 --- a/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs +++ b/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs @@ -6,10 +6,10 @@ public class CopyToContainerParameters // (main.CopyToContainerParameters) [QueryStringParameter("path", true)] public string Path { get; set; } = default!; - [QueryStringParameter("noOverwriteDirNonDir", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("noOverwriteDirNonDir", false)] public bool? AllowOverwriteDirWithFile { get; set; } - [QueryStringParameter("copyUIDGID", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("copyUIDGID", false)] public bool? CopyUIDGID { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs b/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs index 65d412bb..55f3fcf6 100644 --- a/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs @@ -3,22 +3,22 @@ namespace Docker.DotNet.Models { public class ImageBuildParameters // (main.ImageBuildParameters) { - [QueryStringParameter("t", false, typeof(EnumerableQueryStringConverter))] + [QueryStringParameter("t", false)] public IList? Tags { get; set; } - [QueryStringParameter("q", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("q", false)] public bool? SuppressOutput { get; set; } [QueryStringParameter("remote", false)] public string? RemoteContext { get; set; } - [QueryStringParameter("nocache", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("nocache", false)] public bool? NoCache { get; set; } - [QueryStringParameter("rm", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("rm", false)] public bool? Remove { get; set; } - [QueryStringParameter("forcerm", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("forcerm", false)] public bool? ForceRemove { get; set; } [QueryStringParameter("pull", false)] @@ -51,19 +51,19 @@ public class ImageBuildParameters // (main.ImageBuildParameters) [QueryStringParameter("dockerfile", false)] public string? Dockerfile { get; set; } - [QueryStringParameter("buildargs", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("buildargs", false)] public IDictionary? BuildArgs { get; set; } - [QueryStringParameter("labels", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("labels", false)] public IDictionary? Labels { get; set; } - [QueryStringParameter("squash", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("squash", false)] public bool? Squash { get; set; } - [QueryStringParameter("cachefrom", false, typeof(EnumerableQueryStringConverter))] + [QueryStringParameter("cachefrom", false)] public IList? CacheFrom { get; set; } - [QueryStringParameter("extrahosts", false, typeof(EnumerableQueryStringConverter))] + [QueryStringParameter("extrahosts", false)] public IList? ExtraHosts { get; set; } [QueryStringParameter("target", false)] diff --git a/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs b/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs index fae052ba..09f3813c 100644 --- a/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ImageDeleteParameters // (main.ImageDeleteParameters) { - [QueryStringParameter("force", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("force", false)] public bool? Force { get; set; } - [QueryStringParameter("noprune", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("noprune", false)] public bool? NoPrune { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs b/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs index 4b13705a..f0106403 100644 --- a/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ImageLoadParameters // (main.ImageLoadParameters) { - [QueryStringParameter("quiet", true, typeof(BoolQueryStringConverter))] + [QueryStringParameter("quiet", true)] public bool Quiet { get; set; } = default!; } } diff --git a/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs index 03931b53..36ccc26d 100644 --- a/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs @@ -18,7 +18,7 @@ public class ImagesCreateParameters // (main.ImagesCreateParameters) [QueryStringParameter("message", false)] public string? Message { get; set; } - [QueryStringParameter("changes", false, typeof(EnumerableQueryStringConverter))] + [QueryStringParameter("changes", false)] public IList? Changes { get; set; } [QueryStringParameter("platform", false)] diff --git a/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs index c1e26dec..2efce66a 100644 --- a/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs @@ -3,19 +3,19 @@ namespace Docker.DotNet.Models { public class ImagesListParameters // (main.ImagesListParameters) { - [QueryStringParameter("all", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("all", false)] public bool? All { get; set; } - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } - [QueryStringParameter("shared-size", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("shared-size", false)] public bool? SharedSize { get; set; } - [QueryStringParameter("digests", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("digests", false)] public bool? Digests { get; set; } - [QueryStringParameter("manifests", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("manifests", false)] public bool? Manifests { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs index e457204b..7d6968db 100644 --- a/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ImagesPruneParameters // (main.ImagesPruneParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs index 4c6ff82b..680afda8 100644 --- a/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs @@ -9,7 +9,7 @@ public class ImagesSearchParameters // (main.ImagesSearchParameters) [QueryStringParameter("limit", false)] public long? Limit { get; set; } - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs b/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs index 1788f844..6d004995 100644 --- a/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs +++ b/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class NetworksDeleteUnusedParameters // (main.NetworksDeleteUnusedParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs b/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs index 10e2deb0..748bf325 100644 --- a/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class NetworksListParameters // (main.NetworksListParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs b/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs index 305699ef..301c7f66 100644 --- a/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class NodeRemoveParameters // (main.NodeRemoveParameters) { - [QueryStringParameter("force", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs b/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs index ba4deaee..53effdbb 100644 --- a/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs +++ b/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class PluginDisableParameters // (main.PluginDisableParameters) { - [QueryStringParameter("force", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/PluginListParameters.Generated.cs b/src/Docker.DotNet/Models/PluginListParameters.Generated.cs index 89dab338..f67b5704 100644 --- a/src/Docker.DotNet/Models/PluginListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/PluginListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class PluginListParameters // (main.PluginListParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs b/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs index 3076aff2..e3c28174 100644 --- a/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class PluginRemoveParameters // (main.PluginRemoveParameters) { - [QueryStringParameter("force", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs b/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs index 593b0abb..0d26404e 100644 --- a/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ServiceListParameters // (main.ServiceListParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } - [QueryStringParameter("status", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("status", false)] public bool? Status { get; set; } } } diff --git a/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs b/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs index 67e9fadc..01a5650b 100644 --- a/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs @@ -3,25 +3,25 @@ namespace Docker.DotNet.Models { public class ServiceLogsParameters // (main.ServiceLogsParameters) { - [QueryStringParameter("stdout", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stdout", false)] public bool? ShowStdout { get; set; } - [QueryStringParameter("stderr", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("stderr", false)] public bool? ShowStderr { get; set; } [QueryStringParameter("since", false)] public string? Since { get; set; } - [QueryStringParameter("timestamps", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("timestamps", false)] public bool? Timestamps { get; set; } - [QueryStringParameter("follow", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("follow", false)] public bool? Follow { get; set; } [QueryStringParameter("tail", false)] public string? Tail { get; set; } - [QueryStringParameter("details", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("details", false)] public bool? Details { get; set; } } } diff --git a/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs b/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs index a3109483..48a40697 100644 --- a/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class SwarmLeaveParameters // (main.SwarmLeaveParameters) { - [QueryStringParameter("force", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs b/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs index 0d1c6b93..03969603 100644 --- a/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs +++ b/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs @@ -9,13 +9,13 @@ public class SwarmUpdateParameters // (main.SwarmUpdateParameters) [QueryStringParameter("version", true)] public long Version { get; set; } = default!; - [QueryStringParameter("rotateworkertoken", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("rotateworkertoken", false)] public bool? RotateWorkerToken { get; set; } - [QueryStringParameter("rotatemanagertoken", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("rotatemanagertoken", false)] public bool? RotateManagerToken { get; set; } - [QueryStringParameter("rotatemanagerunlockkey", false, typeof(BoolQueryStringConverter))] + [QueryStringParameter("rotatemanagerunlockkey", false)] public bool? RotateManagerUnlockKey { get; set; } } } diff --git a/src/Docker.DotNet/Models/TasksListParameters.Generated.cs b/src/Docker.DotNet/Models/TasksListParameters.Generated.cs index d7d781d0..908fc2f0 100644 --- a/src/Docker.DotNet/Models/TasksListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/TasksListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class TasksListParameters // (main.TasksListParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs b/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs index 49f1c909..34d8ef79 100644 --- a/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class VolumesListParameters // (main.VolumesListParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs b/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs index 6904b653..44881eb2 100644 --- a/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs +++ b/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class VolumesPruneParameters // (main.VolumesPruneParameters) { - [QueryStringParameter("filters", false, typeof(MapQueryStringConverter))] + [QueryStringParameter("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/QueryString.cs b/src/Docker.DotNet/QueryString.cs index 89f68134..fa61471a 100644 --- a/src/Docker.DotNet/QueryString.cs +++ b/src/Docker.DotNet/QueryString.cs @@ -4,14 +4,12 @@ internal class QueryString< #if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] #endif -T> : IQueryString where T : class +T> : IQueryString where T : class, new() { private T Object { get; } private Dictionary AttributedPublicProperties { get; } - private IQueryStringConverterInstanceFactory QueryStringConverterInstanceFactory { get; } - public QueryString(T value) { if (EqualityComparer.Default.Equals(value)) @@ -20,7 +18,6 @@ public QueryString(T value) } Object = value; - QueryStringConverterInstanceFactory = new QueryStringConverterInstanceFactory(); AttributedPublicProperties = FindAttributedPublicProperties(); } @@ -45,20 +42,19 @@ public IDictionary GetKeyValuePairs() { var keyStr = attribute.Name; string[] valueStr; - if (attribute.ConverterType == null) - { - valueStr = [value!.ToString()!]; - } - else + if (attribute.GetConverter() is IQueryStringConverter converter) { - var converter = QueryStringConverterInstanceFactory.GetConverterInstance(attribute.ConverterType); valueStr = ConvertValue(converter, value!); if (valueStr == null) { - throw new InvalidOperationException($"Got null from value converter '{attribute.ConverterType.FullName}'"); + throw new InvalidOperationException($"Got null from value converter '{converter.GetType().FullName}'"); } } + else + { + valueStr = [value!.ToString()!]; + } queryParameters[keyStr] = valueStr; } diff --git a/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs b/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs deleted file mode 100644 index dba24b4e..00000000 --- a/src/Docker.DotNet/QueryStringConverterInstanceFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Docker.DotNet; - -internal class QueryStringConverterInstanceFactory : IQueryStringConverterInstanceFactory -{ - private static readonly ConcurrentDictionary ConverterInstanceRegistry = new ConcurrentDictionary(); - - public IQueryStringConverter GetConverterInstance( -#if NET - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] -#endif - Type t) - { -#pragma warning disable IL2111 // Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. - return ConverterInstanceRegistry.GetOrAdd(t, InitializeConverter); -#pragma warning restore IL2111 // Method with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method. - } - - private static IQueryStringConverter InitializeConverter( -#if NET - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] -#endif - Type t) - { - if (Activator.CreateInstance(t) is IQueryStringConverter instance) - { - return instance; - } - - throw new InvalidOperationException($"Could not get instance of {t.FullName}"); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringParameterAttribute.cs b/src/Docker.DotNet/QueryStringParameterAttribute.cs index e088e39e..a582c932 100644 --- a/src/Docker.DotNet/QueryStringParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringParameterAttribute.cs @@ -1,35 +1,29 @@ namespace Docker.DotNet; [AttributeUsage(AttributeTargets.Property)] -internal sealed class QueryStringParameterAttribute : Attribute +internal class QueryStringParameterAttribute : Attribute { public string Name { get; private set; } public bool IsRequired { get; private set; } -#if NET - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] -#endif - public Type? ConverterType { get; private set; } + public virtual IQueryStringConverter? GetConverter() => null; - public QueryStringParameterAttribute(string name, bool required, -#if NET - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] -#endif - Type? converterType = null) + public QueryStringParameterAttribute(string name, bool required) { if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } - if (converterType != null && !converterType.GetInterfaces().Contains(typeof(IQueryStringConverter))) - { - throw new ArgumentException($"Provided query string converter type is not '{typeof(IQueryStringConverter).FullName}'.", nameof(converterType)); - } - Name = name; IsRequired = required; - ConverterType = converterType; } +} + +internal sealed class QueryStringParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) where TConverter : IQueryStringConverter, new() +{ + private static readonly ConcurrentDictionary ConverterInstanceRegistry = new ConcurrentDictionary(); + + public override IQueryStringConverter GetConverter() => ConverterInstanceRegistry.GetOrAdd(typeof(TConverter), static _ => new TConverter()); } \ No newline at end of file diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index 5b569e04..b2d492c1 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -743,7 +743,18 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { restTag.Name = strings.ToLower(f.Name) } - a := CSAttribute{Type: CSType{"", "QueryStringParameter"}} + queryStringParameter := "QueryStringParameter" + + switch f.Type.Kind() { + case reflect.Bool: + queryStringParameter += "" + case reflect.Slice, reflect.Array: + queryStringParameter += "" + case reflect.Map: + queryStringParameter += "" + } + + a := CSAttribute{Type: CSType{"", queryStringParameter}} a.Arguments = append( a.Arguments, CSArgument{ @@ -752,15 +763,6 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { CSArgument{strconv.FormatBool(restTag.Required), CSInboxTypesMap[reflect.Bool]}) - switch f.Type.Kind() { - case reflect.Bool: - a.Arguments = append(a.Arguments, CSArgument{Value: "typeof(BoolQueryStringConverter)"}) - case reflect.Slice, reflect.Array: - a.Arguments = append(a.Arguments, CSArgument{Value: "typeof(EnumerableQueryStringConverter)"}) - case reflect.Map: - a.Arguments = append(a.Arguments, CSArgument{Value: "typeof(MapQueryStringConverter)"}) - } - csProp.IsOpt = omitEmpty || !restTag.Required csProp.Attributes = append(csProp.Attributes, a) csProp.DefaultValue = restTag.Default From 3446891830ba8d27818ca3e79f83420ca790bcc6 Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 1 Apr 2026 09:53:20 +0200 Subject: [PATCH 03/24] only generate json serialize attribute if model gets json serialized --- src/Docker.DotNet/JsonSerializer.cs | 1 + ...erModelsJsonSerializerContext.Generated.cs | 47 ------------------- tools/specgen/csharptype.go | 3 +- tools/specgen/specgen.go | 12 +++-- 4 files changed, 11 insertions(+), 52 deletions(-) diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 34d5a0c3..04a8a837 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -106,6 +106,7 @@ private static bool TryParseJson(ref ReadOnlySequence buffer, out JsonDocu // extended types that are not generated by source generator [JsonSerializable(typeof(Dictionary[]))] +[JsonSerializable(typeof(IDictionary>))] [JsonSerializable(typeof(ImagesListResponse[]))] [JsonSerializable(typeof(ContainerListResponse[]))] [JsonSerializable(typeof(SwarmService[]))] diff --git a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs index 4f316fc5..6ec7dcda 100644 --- a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs +++ b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs @@ -14,53 +14,32 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(CPUUsage))] [JsonSerializable(typeof(CapacityRange))] [JsonSerializable(typeof(ClusterInfo))] - [JsonSerializable(typeof(ClusterOptions))] [JsonSerializable(typeof(ClusterVolume))] [JsonSerializable(typeof(ClusterVolumeSpec))] [JsonSerializable(typeof(Commit))] - [JsonSerializable(typeof(CommitContainerChangesParameters))] [JsonSerializable(typeof(CommitContainerChangesResponse))] [JsonSerializable(typeof(ComponentVersion))] [JsonSerializable(typeof(ConfigReference))] [JsonSerializable(typeof(ConfigReferenceFileTarget))] - [JsonSerializable(typeof(ConfigReferenceRuntimeTarget))] [JsonSerializable(typeof(ConsoleSize))] - [JsonSerializable(typeof(ContainerAttachParameters))] [JsonSerializable(typeof(ContainerConfig))] - [JsonSerializable(typeof(ContainerEventsParameters))] [JsonSerializable(typeof(ContainerExecCreateParameters))] [JsonSerializable(typeof(ContainerExecCreateResponse))] [JsonSerializable(typeof(ContainerExecInspectResponse))] [JsonSerializable(typeof(ContainerExecStartParameters))] [JsonSerializable(typeof(ContainerFileSystemChangeResponse))] - [JsonSerializable(typeof(ContainerInspectParameters))] [JsonSerializable(typeof(ContainerInspectResponse))] - [JsonSerializable(typeof(ContainerKillParameters))] - [JsonSerializable(typeof(ContainerListProcessesParameters))] [JsonSerializable(typeof(ContainerListResponse))] - [JsonSerializable(typeof(ContainerLogsParameters))] - [JsonSerializable(typeof(ContainerPathStatParameters))] [JsonSerializable(typeof(ContainerPathStatResponse))] [JsonSerializable(typeof(ContainerProcessesResponse))] - [JsonSerializable(typeof(ContainerRemoveParameters))] - [JsonSerializable(typeof(ContainerRenameParameters))] - [JsonSerializable(typeof(ContainerResizeParameters))] - [JsonSerializable(typeof(ContainerRestartParameters))] [JsonSerializable(typeof(ContainerSpec))] - [JsonSerializable(typeof(ContainerStartParameters))] - [JsonSerializable(typeof(ContainerStatsParameters))] [JsonSerializable(typeof(ContainerStatsResponse))] [JsonSerializable(typeof(ContainerStatus))] - [JsonSerializable(typeof(ContainerStopParameters))] - [JsonSerializable(typeof(ContainerUpdateParameters))] [JsonSerializable(typeof(ContainerUpdateResponse))] [JsonSerializable(typeof(ContainerWaitResponse))] [JsonSerializable(typeof(ContainerdInfo))] [JsonSerializable(typeof(ContainerdNamespaces))] - [JsonSerializable(typeof(ContainersListParameters))] - [JsonSerializable(typeof(ContainersPruneParameters))] [JsonSerializable(typeof(ContainersPruneResponse))] - [JsonSerializable(typeof(CopyToContainerParameters))] [JsonSerializable(typeof(CreateContainerParameters))] [JsonSerializable(typeof(CreateContainerResponse))] [JsonSerializable(typeof(CredentialSpec))] @@ -71,7 +50,6 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(DeviceRequest))] [JsonSerializable(typeof(DiscreteGenericResource))] [JsonSerializable(typeof(DispatcherConfig))] - [JsonSerializable(typeof(DockerOCIImageConfig))] [JsonSerializable(typeof(DockerOCIImageConfigExt))] [JsonSerializable(typeof(Driver))] [JsonSerializable(typeof(DriverData))] @@ -87,8 +65,6 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(ExternalCA))] [JsonSerializable(typeof(FirewallInfo))] [JsonSerializable(typeof(GenericResource))] - [JsonSerializable(typeof(GlobalJob))] - [JsonSerializable(typeof(GlobalService))] [JsonSerializable(typeof(Health))] [JsonSerializable(typeof(HealthSummary))] [JsonSerializable(typeof(HealthcheckConfig))] @@ -101,24 +77,17 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(ImageBuildParameters))] [JsonSerializable(typeof(ImageBuildResult))] [JsonSerializable(typeof(ImageConfig))] - [JsonSerializable(typeof(ImageDeleteParameters))] [JsonSerializable(typeof(ImageDeleteResponse))] [JsonSerializable(typeof(ImageHistoryResponse))] [JsonSerializable(typeof(ImageInspectResponse))] - [JsonSerializable(typeof(ImageLoadParameters))] [JsonSerializable(typeof(ImageOptions))] [JsonSerializable(typeof(ImageProperties))] [JsonSerializable(typeof(ImagePropertiesSize))] [JsonSerializable(typeof(ImagePushParameters))] [JsonSerializable(typeof(ImageSearchResponse))] - [JsonSerializable(typeof(ImageTagParameters))] [JsonSerializable(typeof(ImagesCreateParameters))] - [JsonSerializable(typeof(ImagesListParameters))] [JsonSerializable(typeof(ImagesListResponse))] - [JsonSerializable(typeof(ImagesLoadResponse))] - [JsonSerializable(typeof(ImagesPruneParameters))] [JsonSerializable(typeof(ImagesPruneResponse))] - [JsonSerializable(typeof(ImagesSearchParameters))] [JsonSerializable(typeof(IndexInfo))] [JsonSerializable(typeof(Info))] [JsonSerializable(typeof(JSONError))] @@ -153,13 +122,10 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(NetworkingConfig))] [JsonSerializable(typeof(NetworksCreateParameters))] [JsonSerializable(typeof(NetworksCreateResponse))] - [JsonSerializable(typeof(NetworksDeleteUnusedParameters))] - [JsonSerializable(typeof(NetworksListParameters))] [JsonSerializable(typeof(NetworksPruneResponse))] [JsonSerializable(typeof(NodeCSIInfo))] [JsonSerializable(typeof(NodeDescription))] [JsonSerializable(typeof(NodeListResponse))] - [JsonSerializable(typeof(NodeRemoveParameters))] [JsonSerializable(typeof(NodeStatus))] [JsonSerializable(typeof(NodeUpdateParameters))] [JsonSerializable(typeof(OrchestrationConfig))] @@ -175,21 +141,15 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(PluginCapabilityID))] [JsonSerializable(typeof(PluginConfig))] [JsonSerializable(typeof(PluginConfigureParameters))] - [JsonSerializable(typeof(PluginCreateParameters))] [JsonSerializable(typeof(PluginDescription))] [JsonSerializable(typeof(PluginDevice))] - [JsonSerializable(typeof(PluginDisableParameters))] - [JsonSerializable(typeof(PluginEnableParameters))] [JsonSerializable(typeof(PluginEnv))] - [JsonSerializable(typeof(PluginGetPrivilegeParameters))] [JsonSerializable(typeof(PluginInstallParameters))] [JsonSerializable(typeof(PluginInterface))] [JsonSerializable(typeof(PluginLinuxConfig))] - [JsonSerializable(typeof(PluginListParameters))] [JsonSerializable(typeof(PluginMount))] [JsonSerializable(typeof(PluginNetworkConfig))] [JsonSerializable(typeof(PluginPrivilege))] - [JsonSerializable(typeof(PluginRemoveParameters))] [JsonSerializable(typeof(PluginRootFS))] [JsonSerializable(typeof(PluginSettings))] [JsonSerializable(typeof(PluginUpgradeParameters))] @@ -223,8 +183,6 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(ServiceCreateParameters))] [JsonSerializable(typeof(ServiceCreateResponse))] [JsonSerializable(typeof(ServiceInfo))] - [JsonSerializable(typeof(ServiceListParameters))] - [JsonSerializable(typeof(ServiceLogsParameters))] [JsonSerializable(typeof(ServiceMode))] [JsonSerializable(typeof(ServiceSpec))] [JsonSerializable(typeof(ServiceStatus))] @@ -248,7 +206,6 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(SwarmInitParameters))] [JsonSerializable(typeof(SwarmInspectResponse))] [JsonSerializable(typeof(SwarmJoinParameters))] - [JsonSerializable(typeof(SwarmLeaveParameters))] [JsonSerializable(typeof(SwarmLimit))] [JsonSerializable(typeof(SwarmNetwork))] [JsonSerializable(typeof(SwarmPlatform))] @@ -268,13 +225,11 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(TaskResponse))] [JsonSerializable(typeof(TaskSpec))] [JsonSerializable(typeof(TaskStatus))] - [JsonSerializable(typeof(TasksListParameters))] [JsonSerializable(typeof(ThrottleDevice))] [JsonSerializable(typeof(ThrottlingData))] [JsonSerializable(typeof(TmpfsOptions))] [JsonSerializable(typeof(Topology))] [JsonSerializable(typeof(TopologyRequirement))] - [JsonSerializable(typeof(TypeBlock))] [JsonSerializable(typeof(TypeMount))] [JsonSerializable(typeof(Ulimit))] [JsonSerializable(typeof(UpdateConfig))] @@ -290,9 +245,7 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(VolumeSecret))] [JsonSerializable(typeof(VolumeTopology))] [JsonSerializable(typeof(VolumesCreateParameters))] - [JsonSerializable(typeof(VolumesListParameters))] [JsonSerializable(typeof(VolumesListResponse))] - [JsonSerializable(typeof(VolumesPruneParameters))] [JsonSerializable(typeof(VolumesPruneResponse))] [JsonSerializable(typeof(WaitExitError))] [JsonSerializable(typeof(WeightDevice))] diff --git a/tools/specgen/csharptype.go b/tools/specgen/csharptype.go index e58935c6..485a2d78 100644 --- a/tools/specgen/csharptype.go +++ b/tools/specgen/csharptype.go @@ -190,7 +190,8 @@ type CSModelType struct { // yet. it is possible that given the recursive nature that it not be // completed but as long as this is true we will not attempt to generate the // type more than once. - IsStarted bool + IsStarted bool + HasJsonSerializableProperties bool } // NewModel creates a new model type with valid slices diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index b2d492c1..5fe8ac4c 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -771,6 +771,8 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { a.Arguments = append(a.Arguments, CSArgument{jsonName, CSInboxTypesMap[reflect.String]}) csProp.IsOpt = omitEmpty || f.Type.Kind() == reflect.Ptr csProp.Attributes = append(csProp.Attributes, a) + + m.HasJsonSerializableProperties = true } if hasTypeCustomizations { @@ -857,7 +859,7 @@ func main() { reflectType(t) } - names := make([]string, 0, len(reflectedTypes)) + jsonSerializableNames := make([]string, 0, len(reflectedTypes)) for k, v := range reflectedTypes { if _, e := os.Stat(path.Join(sourcePath, v.Name+".Generated.cs")); e == nil { @@ -882,10 +884,12 @@ func main() { f.Close() os.Rename(f.Name(), path.Join(sourcePath, v.Name+".Generated.cs")) - names = append(names, v.Name) + if v.HasJsonSerializableProperties { + jsonSerializableNames = append(jsonSerializableNames, v.Name) + } } - slices.Sort(names) + slices.Sort(jsonSerializableNames) jscf, err := os.Create(path.Join(sourcePath, "DockerModelsJsonSerializerContext.Generated.cs")) if err != nil { @@ -898,7 +902,7 @@ func main() { fmt.Fprintln(jscb, "namespace Docker.DotNet.Models") fmt.Fprintln(jscb, "{") - for _, name := range names { + for _, name := range jsonSerializableNames { fmt.Fprintf(jscb, " [JsonSerializable(typeof(%s))]\n", name) } fmt.Fprintln(jscb, " internal sealed partial class DockerModelsJsonSerializerContext : JsonSerializerContext { }") From f1f6671033d49d139acace645c2466544d8c1b9b Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 1 Apr 2026 10:54:54 +0200 Subject: [PATCH 04/24] add all array types to DockerExtendedJsonSerializerContext also return IList instead of IEnumerable everywhere for consistency --- .../Endpoints/ConfigsOperations.cs | 2 +- .../Endpoints/ISwarmOperations.cs | 4 +-- .../Endpoints/SwarmOperations.cs | 4 +-- src/Docker.DotNet/JsonSerializer.cs | 31 +++++++++++++++++-- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs index d3712136..744f1c8f 100644 --- a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs @@ -11,7 +11,7 @@ internal ConfigOperations(DockerClient client) public async Task> ListConfigsAsync(CancellationToken cancellationToken = default) { - return await _client.MakeRequestAsync>(_client.NoErrorHandlers, HttpMethod.Get, "configs", cancellationToken) + return await _client.MakeRequestAsync(_client.NoErrorHandlers, HttpMethod.Get, "configs", cancellationToken) .ConfigureAwait(false); } diff --git a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs index 1648e5ee..e69c6dec 100644 --- a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs @@ -121,7 +121,7 @@ public interface ISwarmOperations /// 500 - Server error. /// 503 - Node is not part of a swarm. /// - Task> ListServicesAsync(ServiceListParameters? parameters = null, CancellationToken cancellationToken = default); + Task> ListServicesAsync(ServiceListParameters? parameters = null, CancellationToken cancellationToken = default); /// /// Update a service. @@ -198,7 +198,7 @@ public interface ISwarmOperations /// 503 - Node is not part of a swarm. /// /// - Task> ListNodesAsync(CancellationToken cancellationToken = default); + Task> ListNodesAsync(CancellationToken cancellationToken = default); /// /// Inspect a node. diff --git a/src/Docker.DotNet/Endpoints/SwarmOperations.cs b/src/Docker.DotNet/Endpoints/SwarmOperations.cs index 6ff57d85..fa9a77cc 100644 --- a/src/Docker.DotNet/Endpoints/SwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/SwarmOperations.cs @@ -82,7 +82,7 @@ await _client.MakeRequestAsync([NotInSwarmResponseHandler], HttpMethod.Post, "sw .ConfigureAwait(false); } - public async Task> ListServicesAsync(ServiceListParameters? parameters = null, CancellationToken cancellationToken = default) + public async Task> ListServicesAsync(ServiceListParameters? parameters = null, CancellationToken cancellationToken = default) { var queryParameters = parameters == null ? null : new QueryString(parameters); @@ -193,7 +193,7 @@ private static Dictionary RegistryAuthHeaders(AuthConfig? authCo }; } - public async Task> ListNodesAsync(CancellationToken cancellationToken = default) + public async Task> ListNodesAsync(CancellationToken cancellationToken = default) { return await _client.MakeRequestAsync([NotInSwarmResponseHandler], HttpMethod.Get, "nodes", cancellationToken) .ConfigureAwait(false); diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 04a8a837..bd4e2514 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -105,10 +105,35 @@ private static bool TryParseJson(ref ReadOnlySequence buffer, out JsonDocu } // extended types that are not generated by source generator -[JsonSerializable(typeof(Dictionary[]))] +/// Filters [JsonSerializable(typeof(IDictionary>))] -[JsonSerializable(typeof(ImagesListResponse[]))] + +/// +[JsonSerializable(typeof(SwarmConfig[]))] +/// [JsonSerializable(typeof(ContainerListResponse[]))] +/// +[JsonSerializable(typeof(ContainerFileSystemChangeResponse[]))] + +/// +[JsonSerializable(typeof(ImagesListResponse[]))] +/// +[JsonSerializable(typeof(ImageHistoryResponse[]))] +/// +[JsonSerializable(typeof(Dictionary[]))] +/// +[JsonSerializable(typeof(ImageSearchResponse[]))] + +/// +[JsonSerializable(typeof(NetworkResponse[]))] + +/// +[JsonSerializable(typeof(Plugin[]))] +/// +[JsonSerializable(typeof(PluginPrivilege[]))] + +/// [JsonSerializable(typeof(SwarmService[]))] -[JsonSerializable(typeof(IList))] +/// +[JsonSerializable(typeof(NodeListResponse[]))] internal sealed partial class DockerExtendedJsonSerializerContext : JsonSerializerContext { } From f1aff52f80ed9845bb78452f7e8cba937b76a03c Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 1 Apr 2026 11:11:16 +0200 Subject: [PATCH 05/24] move System.Text.Json.Serialization.Metadata to csproj Using --- src/Docker.DotNet/Docker.DotNet.csproj | 3 ++- src/Docker.DotNet/DockerClient.cs | 2 -- src/Docker.DotNet/JsonSerializer.cs | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 5ce06281..1430a200 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -1,4 +1,4 @@ - + Docker.DotNet Docker.DotNet.Enhanced @@ -50,6 +50,7 @@ + diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index d9f1b1b5..6328e8f7 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -1,7 +1,5 @@ namespace Docker.DotNet; -using System; - public sealed class DockerClient : IDockerClient { internal readonly IEnumerable NoErrorHandlers = Enumerable.Empty(); diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index bd4e2514..711a9fff 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization.Metadata; - namespace Docker.DotNet; internal sealed class JsonSerializer From b9fa870fedf8dab7867e118c556af87cc3281d2c Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 1 Apr 2026 12:00:34 +0200 Subject: [PATCH 06/24] remove ConcurrentDictionary and use static field instead for query string converter --- src/Docker.DotNet/QueryStringParameterAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Docker.DotNet/QueryStringParameterAttribute.cs b/src/Docker.DotNet/QueryStringParameterAttribute.cs index a582c932..07159b1e 100644 --- a/src/Docker.DotNet/QueryStringParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringParameterAttribute.cs @@ -23,7 +23,7 @@ public QueryStringParameterAttribute(string name, bool required) internal sealed class QueryStringParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) where TConverter : IQueryStringConverter, new() { - private static readonly ConcurrentDictionary ConverterInstanceRegistry = new ConcurrentDictionary(); + private static TConverter? ConverterInstance; - public override IQueryStringConverter GetConverter() => ConverterInstanceRegistry.GetOrAdd(typeof(TConverter), static _ => new TConverter()); + public override IQueryStringConverter GetConverter() => ConverterInstance ??= new TConverter(); } \ No newline at end of file From dc0a331fbd24f5f945cfbbaec015790ddbbcf13a Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 12 Apr 2026 09:28:05 +0200 Subject: [PATCH 07/24] bump xunit to 4.0.0-pre.81 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3bb23f85..9e560068 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - - + + From e4cc73fdd8371f3dd6110684f6e7a705b55941c4 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:08:43 +0200 Subject: [PATCH 08/24] chore: apply minor clean ups --- src/Directory.Build.props | 2 +- src/Docker.DotNet/Docker.DotNet.csproj | 2 +- src/Docker.DotNet/JsonSerializer.cs | 33 ++++++++++--------- src/Docker.DotNet/QueryString.cs | 10 +++--- .../QueryStringParameterAttribute.cs | 7 ++-- .../Docker.DotNet.Tests.csproj | 4 +-- .../JsonRequestContentTests.cs | 7 ++-- .../Docker.DotNet.TestsV2.csproj | 4 +-- 8 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a6367c51..4b3a7d31 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,7 @@ + true true net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 Copyright (c) .NET Foundation and Contributors; Andre Hofmeister @@ -20,7 +21,6 @@ enable - true diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 1430a200..cfc94156 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -1,4 +1,4 @@ - + Docker.DotNet Docker.DotNet.Enhanced diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 711a9fff..9e303aff 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -17,8 +17,7 @@ private JsonSerializer() { _options.TypeInfoResolver = JsonTypeInfoResolver.Combine( DockerModelsJsonSerializerContext.Default, - DockerExtendedJsonSerializerContext.Default - ); + DockerExtendedJsonSerializerContext.Default); _options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; _options.Converters.Add(new JsonEnumMemberConverter()); _options.Converters.Add(new JsonEnumMemberConverter()); @@ -102,36 +101,38 @@ private static bool TryParseJson(ref ReadOnlySequence buffer, out JsonDocu } } -// extended types that are not generated by source generator -/// Filters +// Additional source-generated metadata for collections and dictionaries used by operations. + +// Filters [JsonSerializable(typeof(IDictionary>))] -/// +// ConfigOperations.ListConfigsAsync [JsonSerializable(typeof(SwarmConfig[]))] -/// + +// ContainerOperations.ListContainersAsync [JsonSerializable(typeof(ContainerListResponse[]))] -/// +// ContainerOperations.InspectChangesAsync [JsonSerializable(typeof(ContainerFileSystemChangeResponse[]))] -/// +// ImageOperations.ListImagesAsync [JsonSerializable(typeof(ImagesListResponse[]))] -/// +// ImageOperations.GetImageHistoryAsync [JsonSerializable(typeof(ImageHistoryResponse[]))] -/// +// ImageOperations.DeleteImageAsync [JsonSerializable(typeof(Dictionary[]))] -/// +// ImageOperations.SearchImagesAsync [JsonSerializable(typeof(ImageSearchResponse[]))] -/// +// NetworkOperations.ListNetworksAsync [JsonSerializable(typeof(NetworkResponse[]))] -/// +// PluginOperations.ListPluginsAsync [JsonSerializable(typeof(Plugin[]))] -/// +// PluginOperations.GetPrivilegesAsync [JsonSerializable(typeof(PluginPrivilege[]))] -/// +// SwarmOperations.ListServicesAsync [JsonSerializable(typeof(SwarmService[]))] -/// +// SwarmOperations.ListNodesAsync [JsonSerializable(typeof(NodeListResponse[]))] internal sealed partial class DockerExtendedJsonSerializerContext : JsonSerializerContext { } diff --git a/src/Docker.DotNet/QueryString.cs b/src/Docker.DotNet/QueryString.cs index fa61471a..9a74e019 100644 --- a/src/Docker.DotNet/QueryString.cs +++ b/src/Docker.DotNet/QueryString.cs @@ -4,7 +4,7 @@ internal class QueryString< #if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] #endif -T> : IQueryString where T : class, new() +T> : IQueryString where T : class { private T Object { get; } @@ -12,7 +12,7 @@ internal class QueryString< public QueryString(T value) { - if (EqualityComparer.Default.Equals(value)) + if (value is null) { throw new ArgumentNullException(nameof(value)); } @@ -33,7 +33,7 @@ public IDictionary GetKeyValuePairs() // 'Required' check if (attribute.IsRequired && value == null) { - string propertyFullName = $"{property.GetType().FullName}.{property.Name}"; + string propertyFullName = $"{property.DeclaringType?.FullName}.{property.Name}"; throw new ArgumentException("Got null/unset value for a required query parameter.", propertyFullName); } @@ -42,7 +42,7 @@ public IDictionary GetKeyValuePairs() { var keyStr = attribute.Name; string[] valueStr; - if (attribute.GetConverter() is IQueryStringConverter converter) + if (attribute.GetConverter() is { } converter) { valueStr = ConvertValue(converter, value!); @@ -118,7 +118,7 @@ private static Dictionary FindAttributedPublicPropert } #if NET - [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Activator.CreateInstance is only used for value types here; safe for runtime usage.")] + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Activator.CreateInstance is only used for value types here, safe for runtime usage.")] #endif private static bool IsDefaultOfType(object? o) { diff --git a/src/Docker.DotNet/QueryStringParameterAttribute.cs b/src/Docker.DotNet/QueryStringParameterAttribute.cs index 07159b1e..8e73e463 100644 --- a/src/Docker.DotNet/QueryStringParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringParameterAttribute.cs @@ -21,9 +21,10 @@ public QueryStringParameterAttribute(string name, bool required) } } -internal sealed class QueryStringParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) where TConverter : IQueryStringConverter, new() +internal sealed class QueryStringParameterAttribute(string name, bool required) + : QueryStringParameterAttribute(name, required) where TConverter : IQueryStringConverter, new() { - private static TConverter? ConverterInstance; + private static TConverter? _converterInstance; - public override IQueryStringConverter GetConverter() => ConverterInstance ??= new TConverter(); + public override IQueryStringConverter GetConverter() => _converterInstance ??= new TConverter(); } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 190d1748..064c3b68 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0;net10.0 linux-arm64 @@ -51,6 +51,6 @@ - + diff --git a/test/Docker.DotNet.Tests/JsonRequestContentTests.cs b/test/Docker.DotNet.Tests/JsonRequestContentTests.cs index ef61bb41..3a9f1c25 100644 --- a/test/Docker.DotNet.Tests/JsonRequestContentTests.cs +++ b/test/Docker.DotNet.Tests/JsonRequestContentTests.cs @@ -5,20 +5,21 @@ public sealed class JsonRequestContentTests [Fact] public void Constructor_ThrowsArgumentNullException_WhenValueIsNull() { - Assert.Throws(() => new JsonRequestContent(null, JsonSerializer.Instance)); + Assert.Throws(() => new JsonRequestContent(null!, JsonSerializer.Instance)); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenSerializerIsNull() { - Assert.Throws(() => new JsonRequestContent(new object(), null)); + Assert.Throws(() => new JsonRequestContent(new object(), null!)); } [Fact] public async Task GetContent_Succeeds_WhenValueAndSerializerAreValid() { - var content = new JsonRequestContent[]>([new Dictionary() { { "key", "value" } }], JsonSerializer.Instance); + var content = new JsonRequestContent[]>([new Dictionary { { "key", "value" } }], JsonSerializer.Instance); using var httpContent = content.GetContent(); + Assert.NotNull(httpContent.Headers.ContentType); Assert.Equal("application/json; charset=utf-8", httpContent.Headers.ContentType.ToString()); var jsonString = await httpContent.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Equal("""[{"key":"value"}]""", jsonString); diff --git a/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj b/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj index 388cc7cd..b5a18285 100644 --- a/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj +++ b/test/Docker.DotNet.TestsV2/Docker.DotNet.TestsV2.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0;net10.0 linux-arm64 @@ -38,6 +38,6 @@ - + From 133b799f5a22f3ea0d30ceee203715b480d8c550 Mon Sep 17 00:00:00 2001 From: campersau Date: Fri, 17 Apr 2026 22:44:57 +0200 Subject: [PATCH 09/24] TestFixture remove IDisposable --- test/Docker.DotNet.Tests/TestFixture.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 2c5c5205..47f59ec5 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Tests; [CollectionDefinition(nameof(TestCollection))] public sealed class TestCollection : ICollectionFixture; -public sealed class TestFixture : Progress, IAsyncLifetime, IDisposable, ILogger +public sealed class TestFixture : Progress, IAsyncLifetime, ILogger { private const LogLevel MinLogLevel = LogLevel.Debug; @@ -149,11 +149,7 @@ await DockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true await DockerClient.Images.DeleteImageAsync(image.ID, new ImageDeleteParameters { Force = true }, Cts.Token) .ConfigureAwait(false); } - } - /// - public void Dispose() - { Cts.Dispose(); DockerClient.Dispose(); } From bb7320f7bc0a5cbbfa858d227bff33f2eab361f4 Mon Sep 17 00:00:00 2001 From: campersau Date: Fri, 17 Apr 2026 21:26:44 +0000 Subject: [PATCH 10/24] JsonSerializable for inline and anonymous types --- ...ockerModelsJsonSerializerContext.Generated.cs | 9 +++++++++ tools/specgen/specgen.go | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs index 6ec7dcda..f00147c7 100644 --- a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs +++ b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs @@ -14,13 +14,16 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(CPUUsage))] [JsonSerializable(typeof(CapacityRange))] [JsonSerializable(typeof(ClusterInfo))] + [JsonSerializable(typeof(ClusterOptions))] [JsonSerializable(typeof(ClusterVolume))] [JsonSerializable(typeof(ClusterVolumeSpec))] [JsonSerializable(typeof(Commit))] + [JsonSerializable(typeof(CommitContainerChangesParameters))] [JsonSerializable(typeof(CommitContainerChangesResponse))] [JsonSerializable(typeof(ComponentVersion))] [JsonSerializable(typeof(ConfigReference))] [JsonSerializable(typeof(ConfigReferenceFileTarget))] + [JsonSerializable(typeof(ConfigReferenceRuntimeTarget))] [JsonSerializable(typeof(ConsoleSize))] [JsonSerializable(typeof(ContainerConfig))] [JsonSerializable(typeof(ContainerExecCreateParameters))] @@ -35,6 +38,7 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(ContainerSpec))] [JsonSerializable(typeof(ContainerStatsResponse))] [JsonSerializable(typeof(ContainerStatus))] + [JsonSerializable(typeof(ContainerUpdateParameters))] [JsonSerializable(typeof(ContainerUpdateResponse))] [JsonSerializable(typeof(ContainerWaitResponse))] [JsonSerializable(typeof(ContainerdInfo))] @@ -50,6 +54,7 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(DeviceRequest))] [JsonSerializable(typeof(DiscreteGenericResource))] [JsonSerializable(typeof(DispatcherConfig))] + [JsonSerializable(typeof(DockerOCIImageConfig))] [JsonSerializable(typeof(DockerOCIImageConfigExt))] [JsonSerializable(typeof(Driver))] [JsonSerializable(typeof(DriverData))] @@ -65,6 +70,8 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(ExternalCA))] [JsonSerializable(typeof(FirewallInfo))] [JsonSerializable(typeof(GenericResource))] + [JsonSerializable(typeof(GlobalJob))] + [JsonSerializable(typeof(GlobalService))] [JsonSerializable(typeof(Health))] [JsonSerializable(typeof(HealthSummary))] [JsonSerializable(typeof(HealthcheckConfig))] @@ -87,6 +94,7 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(ImageSearchResponse))] [JsonSerializable(typeof(ImagesCreateParameters))] [JsonSerializable(typeof(ImagesListResponse))] + [JsonSerializable(typeof(ImagesLoadResponse))] [JsonSerializable(typeof(ImagesPruneResponse))] [JsonSerializable(typeof(IndexInfo))] [JsonSerializable(typeof(Info))] @@ -230,6 +238,7 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(TmpfsOptions))] [JsonSerializable(typeof(Topology))] [JsonSerializable(typeof(TopologyRequirement))] + [JsonSerializable(typeof(TypeBlock))] [JsonSerializable(typeof(TypeMount))] [JsonSerializable(typeof(Ulimit))] [JsonSerializable(typeof(UpdateConfig))] diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index 5fe8ac4c..a314c508 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -652,8 +652,9 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { inlineStructName := t.Name() + f.Name inlineModel := &CSModelType{ - Name: inlineStructName, - SourceName: fmt.Sprintf("%s.%s", t, f.Name), + Name: inlineStructName, + SourceName: fmt.Sprintf("%s.%s", t, f.Name), + HasJsonSerializableProperties: true, } reflectTypeMembers(f.Type, inlineModel) @@ -679,6 +680,8 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { }) m.Properties = append(m.Properties, csProp) + + m.HasJsonSerializableProperties = true } else if f.Anonymous { // If the type is anonymous we need to inline its values to this model. clen := len(m.Constructors) @@ -698,6 +701,10 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { // Now we need to add in all of the inherited types parameters m.Properties = append(m.Properties, newType.Properties...) + + if newType.HasJsonSerializableProperties { + m.HasJsonSerializableProperties = true + } } else { // If we are referencing a struct that isnt inline or anonymous we need to update it too. if ut := ultimateType(f.Type); ut.Kind() == reflect.Struct { @@ -788,6 +795,11 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { m.Properties = append(m.Properties, csProp) } } + + // If we have no properties, we still want to generate a JsonSerializerContext for this type, so we mark it as having json serializable properties to ensure that happens. + if len(m.Properties) == 0 { + m.HasJsonSerializableProperties = true + } } func reflectType(t reflect.Type) { From 858393a5e8c7c3145e26cc0042934393d14ae2a5 Mon Sep 17 00:00:00 2001 From: campersau Date: Sat, 18 Apr 2026 23:50:42 +0200 Subject: [PATCH 11/24] Add JsonTypeInfoCache --- src/Docker.DotNet/JsonSerializer.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 9e303aff..063f5101 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -42,26 +42,27 @@ public HttpContent GetHttpContent(T value) public string Serialize(T value) { - return System.Text.Json.JsonSerializer.Serialize(value, (JsonTypeInfo)_options.GetTypeInfo(typeof(T))); + return System.Text.Json.JsonSerializer.Serialize(value, JsonTypeInfoCache.Value); } public byte[] SerializeToUtf8Bytes(T value) { - return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, (JsonTypeInfo)_options.GetTypeInfo(typeof(T))); + return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, JsonTypeInfoCache.Value); } public T Deserialize(byte[] json) { - return System.Text.Json.JsonSerializer.Deserialize(json, (JsonTypeInfo)_options.GetTypeInfo(typeof(T)))!; + return System.Text.Json.JsonSerializer.Deserialize(json, JsonTypeInfoCache.Value)!; } public Task DeserializeAsync(HttpContent content, CancellationToken cancellationToken) { - return content.ReadFromJsonAsync((JsonTypeInfo)_options.GetTypeInfo(typeof(T)), cancellationToken)!; + return content.ReadFromJsonAsync(JsonTypeInfoCache.Value, cancellationToken)!; } public async IAsyncEnumerable DeserializeAsync(Stream stream, [EnumeratorCancellation] CancellationToken cancellationToken) { + var jsonTypeInfo = JsonTypeInfoCache.Value; var reader = PipeReader.Create(stream); while (true) @@ -73,7 +74,7 @@ public async IAsyncEnumerable DeserializeAsync(Stream stream, [EnumeratorC while (!buffer.IsEmpty && TryParseJson(ref buffer, out var jsonDocument)) { - yield return jsonDocument!.Deserialize((JsonTypeInfo)_options.GetTypeInfo(typeof(T)))!; + yield return jsonDocument!.Deserialize(jsonTypeInfo)!; } if (result.IsCompleted) @@ -99,6 +100,11 @@ private static bool TryParseJson(ref ReadOnlySequence buffer, out JsonDocu return false; } + + private static class JsonTypeInfoCache + { + public static readonly JsonTypeInfo Value = (JsonTypeInfo)Instance._options.GetTypeInfo(typeof(T)); + } } // Additional source-generated metadata for collections and dictionaries used by operations. From 0c002966cf4faf9fff76f4e1e4bcf41090c5122d Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 19 Apr 2026 15:07:29 +0200 Subject: [PATCH 12/24] Use an analyzer for checking DockerExtendedJsonSerializerContext Attributes --- Directory.Packages.props | 2 + Docker.DotNet.slnx | 3 + src/Docker.DotNet/Docker.DotNet.csproj | 1 + .../Docker.DotNet.SourceAnalyzers.csproj | 22 +++++ .../JsonSerializerAnalyzer.cs | 83 +++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj create mode 100644 tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 9e560068..fccf6d9c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,8 @@ + + diff --git a/Docker.DotNet.slnx b/Docker.DotNet.slnx index 2da18906..89c36932 100644 --- a/Docker.DotNet.slnx +++ b/Docker.DotNet.slnx @@ -23,4 +23,7 @@ + + + diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index cfc94156..eb7b338d 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -21,6 +21,7 @@ + diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj new file mode 100644 index 00000000..9ae4339d --- /dev/null +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + true + true + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs new file mode 100644 index 00000000..cf7702f3 --- /dev/null +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -0,0 +1,83 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Linq; + +namespace Docker.DotNet.SourceAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class JsonSerializerAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "DDN001"; + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + "Missing JsonSerializable attribute", + "Type '{0}' used in Docker.DotNet.DockerClient.MakeRequestAsync is not registered in the DockerExtendedJsonSerializerContext", + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + + if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) + return; + + if (methodSymbol.Name != "MakeRequestAsync") + return; + + var containingType = methodSymbol.ContainingType; + if (containingType?.ToDisplayString() != "Docker.DotNet.DockerClient") + return; + + if (methodSymbol.TypeArguments.Length == 0) + return; + + var targetType = methodSymbol.TypeArguments[0]; + + if (targetType.TypeKind == TypeKind.TypeParameter) + return; + + if (targetType.Name == "NoContent" && targetType.ContainingType?.ToDisplayString() == "Docker.DotNet.DockerClient") + return; + + var jsonSerializableAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute"); + if (jsonSerializableAttributeSymbol == null) + return; + + var isRegistered = false; + + foreach (var jsonSerializerContextTypeName in new[] { "Docker.DotNet.DockerExtendedJsonSerializerContext", "Docker.DotNet.Models.DockerModelsJsonSerializerContext" }) + { + var jsonSerializerContextType = context.Compilation.GetTypeByMetadataName(jsonSerializerContextTypeName); + if (jsonSerializerContextType == null) continue; + + isRegistered = + jsonSerializerContextType.GetAttributes().Any(attr => + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonSerializableAttributeSymbol) && + attr.ConstructorArguments.Any(arg => + arg.Value is ITypeSymbol registeredType && + SymbolEqualityComparer.Default.Equals(registeredType, targetType))); + + if (isRegistered) break; + } + + if (!isRegistered) + { + var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation(), targetType.ToDisplayString()); + context.ReportDiagnostic(diagnostic); + } + } +} From 2b87054129e38d0e00447f954bdadf83a25500d7 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 19 Apr 2026 15:11:38 +0200 Subject: [PATCH 13/24] Add missing json serializer types: string, Secret[], TaskResponse[] --- src/Docker.DotNet/Endpoints/SecretsOperations.cs | 2 +- src/Docker.DotNet/Endpoints/TasksOperations.cs | 2 +- src/Docker.DotNet/JsonSerializer.cs | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/SecretsOperations.cs b/src/Docker.DotNet/Endpoints/SecretsOperations.cs index 01e36b2e..58b77750 100644 --- a/src/Docker.DotNet/Endpoints/SecretsOperations.cs +++ b/src/Docker.DotNet/Endpoints/SecretsOperations.cs @@ -11,7 +11,7 @@ internal SecretsOperations(DockerClient client) public async Task> ListAsync(CancellationToken cancellationToken = default) { - return await _client.MakeRequestAsync>(_client.NoErrorHandlers, HttpMethod.Get, "secrets", cancellationToken) + return await _client.MakeRequestAsync(_client.NoErrorHandlers, HttpMethod.Get, "secrets", cancellationToken) .ConfigureAwait(false); } diff --git a/src/Docker.DotNet/Endpoints/TasksOperations.cs b/src/Docker.DotNet/Endpoints/TasksOperations.cs index 1fe89878..c50a3c76 100644 --- a/src/Docker.DotNet/Endpoints/TasksOperations.cs +++ b/src/Docker.DotNet/Endpoints/TasksOperations.cs @@ -18,7 +18,7 @@ public async Task> ListAsync(TasksListParameters? parameters { var queryParameters = parameters == null ? null : new QueryString(parameters); - return await _client.MakeRequestAsync>(_client.NoErrorHandlers, HttpMethod.Get, "tasks", queryParameters, cancellationToken) + return await _client.MakeRequestAsync(_client.NoErrorHandlers, HttpMethod.Get, "tasks", queryParameters, cancellationToken) .ConfigureAwait(false); } diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 063f5101..8c5fee04 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -109,6 +109,8 @@ private static class JsonTypeInfoCache // Additional source-generated metadata for collections and dictionaries used by operations. +[JsonSerializable(typeof(string))] + // Filters [JsonSerializable(typeof(IDictionary>))] @@ -137,8 +139,14 @@ private static class JsonTypeInfoCache // PluginOperations.GetPrivilegesAsync [JsonSerializable(typeof(PluginPrivilege[]))] +// SecretOperations.ListAsync +[JsonSerializable(typeof(Secret[]))] + // SwarmOperations.ListServicesAsync [JsonSerializable(typeof(SwarmService[]))] // SwarmOperations.ListNodesAsync [JsonSerializable(typeof(NodeListResponse[]))] + +// TaskOperations.ListAsync +[JsonSerializable(typeof(TaskResponse[]))] internal sealed partial class DockerExtendedJsonSerializerContext : JsonSerializerContext { } From a5aac93eb3d11038a5d7c8e97e1d2e74d9ff1e14 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 19 Apr 2026 15:17:06 +0200 Subject: [PATCH 14/24] Analyzer project cleanup --- .../Docker.DotNet.SourceAnalyzers.csproj | 16 ++++++++++------ .../JsonSerializerAnalyzer.cs | 12 ++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj index 9ae4339d..eee54af7 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -1,12 +1,10 @@  - netstandard2.0 - true true false + false - all @@ -14,9 +12,15 @@ - + + + + + + + + - - \ No newline at end of file + diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs index cf7702f3..dd9ebc23 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -1,11 +1,4 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using System.Collections.Immutable; -using System.Linq; - -namespace Docker.DotNet.SourceAnalyzers; +namespace Docker.DotNet.SourceAnalyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class JsonSerializerAnalyzer : DiagnosticAnalyzer @@ -38,8 +31,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) if (methodSymbol.Name != "MakeRequestAsync") return; - var containingType = methodSymbol.ContainingType; - if (containingType?.ToDisplayString() != "Docker.DotNet.DockerClient") + if (methodSymbol.ContainingType?.ToDisplayString() != "Docker.DotNet.DockerClient") return; if (methodSymbol.TypeArguments.Length == 0) From b7bb2f93adce81f6a5af5f2a3273ddb44d630d65 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 19 Apr 2026 16:04:41 +0200 Subject: [PATCH 15/24] Handle MakeRequestAsync / MonitorStreamForMessagesAsync / JsonSerializer / new JsonRequestContent --- src/Docker.DotNet/JsonSerializer.cs | 6 ++ .../Docker.DotNet.SourceAnalyzers.csproj | 2 +- .../JsonSerializerAnalyzer.cs | 88 +++++++++++++------ 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 8c5fee04..f2183e36 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -110,9 +110,11 @@ private static class JsonTypeInfoCache // Additional source-generated metadata for collections and dictionaries used by operations. [JsonSerializable(typeof(string))] +[JsonSerializable(typeof(IList))] // Filters [JsonSerializable(typeof(IDictionary>))] +[JsonSerializable(typeof(IDictionary))] // ConfigOperations.ListConfigsAsync [JsonSerializable(typeof(SwarmConfig[]))] @@ -130,6 +132,8 @@ private static class JsonTypeInfoCache [JsonSerializable(typeof(Dictionary[]))] // ImageOperations.SearchImagesAsync [JsonSerializable(typeof(ImageSearchResponse[]))] +// ImageOperations.RegistryConfigHeaders +[JsonSerializable(typeof(Dictionary))] // NetworkOperations.ListNetworksAsync [JsonSerializable(typeof(NetworkResponse[]))] @@ -138,6 +142,8 @@ private static class JsonTypeInfoCache [JsonSerializable(typeof(Plugin[]))] // PluginOperations.GetPrivilegesAsync [JsonSerializable(typeof(PluginPrivilege[]))] +// PluginOperations.InstallPluginAsync +[JsonSerializable(typeof(IList))] // SecretOperations.ListAsync [JsonSerializable(typeof(Secret[]))] diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj index eee54af7..3c329327 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + netstandard2.0;net8.0 true false false diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs index dd9ebc23..5eb40e21 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -7,7 +7,7 @@ public sealed class JsonSerializerAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, "Missing JsonSerializable attribute", - "Type '{0}' used in Docker.DotNet.DockerClient.MakeRequestAsync is not registered in the DockerExtendedJsonSerializerContext", + "Type '{0}' used in {1} is not registered in DockerExtendedJsonSerializerContext", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -18,7 +18,12 @@ public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + + // Handle method calls (MakeRequestAsync, etc.) context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + + // Handle 'new JsonRequestContent' + context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); } private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) @@ -28,48 +33,77 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) return; - if (methodSymbol.Name != "MakeRequestAsync") - return; + bool isStjEntryPoint = + (methodSymbol.Name == "MakeRequestAsync" && methodSymbol.ContainingType?.Name == "DockerClient") || + (methodSymbol.Name == "MonitorStreamForMessagesAsync" && methodSymbol.ContainingType?.Name == "StreamUtil") || + (methodSymbol.ContainingType?.Name == "JsonSerializer" && methodSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet"); - if (methodSymbol.ContainingType?.ToDisplayString() != "Docker.DotNet.DockerClient") - return; + if (isStjEntryPoint && methodSymbol.TypeArguments.Length == 1) + { + var methodName = $"{methodSymbol.ContainingType.Name}.{methodSymbol.Name}"; + CheckAndReportType(context, methodSymbol.TypeArguments[0], invocation.GetLocation(), methodName); + } + } - if (methodSymbol.TypeArguments.Length == 0) + private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) + { + var creation = (ObjectCreationExpressionSyntax)context.Node; + + if (context.SemanticModel.GetSymbolInfo(creation).Symbol is not IMethodSymbol constructorSymbol) return; - var targetType = methodSymbol.TypeArguments[0]; + var typeSymbol = constructorSymbol.ContainingType; - if (targetType.TypeKind == TypeKind.TypeParameter) - return; + // Check if we are instantiating JsonRequestContent + if (typeSymbol.Name == "JsonRequestContent" && + typeSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet") + { + if (typeSymbol.TypeArguments.Length == 1) + { + var typeName = typeSymbol.Name; + CheckAndReportType(context, typeSymbol.TypeArguments[0], creation.GetLocation(), typeName); + } + } + } - if (targetType.Name == "NoContent" && targetType.ContainingType?.ToDisplayString() == "Docker.DotNet.DockerClient") + private static void CheckAndReportType(SyntaxNodeAnalysisContext context, ITypeSymbol targetType, Location location, string sourceName) + { + // Skip generic type parameters, Object, and the internal NoContent marker + if (targetType.TypeKind == TypeKind.TypeParameter || + targetType.SpecialType == SpecialType.System_Object || + targetType.Name == "NoContent") + { return; + } - var jsonSerializableAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute"); - if (jsonSerializableAttributeSymbol == null) - return; + var jsonSerializableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute"); + if (jsonSerializableAttributeTypeSymbol == null) return; var isRegistered = false; - - foreach (var jsonSerializerContextTypeName in new[] { "Docker.DotNet.DockerExtendedJsonSerializerContext", "Docker.DotNet.Models.DockerModelsJsonSerializerContext" }) + var jsonSerializerContextNames = new[] { - var jsonSerializerContextType = context.Compilation.GetTypeByMetadataName(jsonSerializerContextTypeName); - if (jsonSerializerContextType == null) continue; + "Docker.DotNet.DockerExtendedJsonSerializerContext", + "Docker.DotNet.Models.DockerModelsJsonSerializerContext" + }; - isRegistered = - jsonSerializerContextType.GetAttributes().Any(attr => - SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonSerializableAttributeSymbol) && - attr.ConstructorArguments.Any(arg => - arg.Value is ITypeSymbol registeredType && - SymbolEqualityComparer.Default.Equals(registeredType, targetType))); - - if (isRegistered) break; + foreach (var jsonSerializerContextName in jsonSerializerContextNames) + { + var jsonSerializerContextTypeSymbol = context.Compilation.GetTypeByMetadataName(jsonSerializerContextName); + if (jsonSerializerContextTypeSymbol == null) continue; + + if (jsonSerializerContextTypeSymbol.GetAttributes().Any(attr => + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonSerializableAttributeTypeSymbol) && + attr.ConstructorArguments.Any(arg => + arg.Value is ITypeSymbol reg && SymbolEqualityComparer.Default.Equals(reg, targetType)))) + { + isRegistered = true; + break; + } } if (!isRegistered) { - var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation(), targetType.ToDisplayString()); - context.ReportDiagnostic(diagnostic); + context.ReportDiagnostic(Diagnostic.Create(Rule, location, targetType.ToDisplayString(), sourceName)); } } } From dd17765d4b1f76c280978f986f37bc88e7e839ea Mon Sep 17 00:00:00 2001 From: campersau Date: Mon, 20 Apr 2026 10:40:39 +0200 Subject: [PATCH 16/24] Refactor QueryStringParameterAttribute to avoid a JsonSerializer.Serialize(object) call --- src/Docker.DotNet/IQueryStringConverter.cs | 8 ----- ...mitContainerChangesParameters.Generated.cs | 4 +-- .../ContainerAttachParameters.Generated.cs | 10 +++--- .../ContainerEventsParameters.Generated.cs | 2 +- .../ContainerInspectParameters.Generated.cs | 2 +- .../ContainerLogsParameters.Generated.cs | 8 ++--- .../ContainerRemoveParameters.Generated.cs | 6 ++-- .../ContainerStatsParameters.Generated.cs | 4 +-- .../ContainersListParameters.Generated.cs | 6 ++-- .../ContainersPruneParameters.Generated.cs | 2 +- .../CopyToContainerParameters.Generated.cs | 4 +-- .../Models/ImageBuildParameters.Generated.cs | 20 +++++------ .../Models/ImageDeleteParameters.Generated.cs | 4 +-- .../Models/ImageLoadParameters.Generated.cs | 2 +- .../ImagesCreateParameters.Generated.cs | 2 +- .../Models/ImagesListParameters.Generated.cs | 10 +++--- .../Models/ImagesPruneParameters.Generated.cs | 2 +- .../ImagesSearchParameters.Generated.cs | 2 +- ...etworksDeleteUnusedParameters.Generated.cs | 2 +- .../NetworksListParameters.Generated.cs | 2 +- .../Models/NodeRemoveParameters.Generated.cs | 2 +- .../PluginDisableParameters.Generated.cs | 2 +- .../Models/PluginListParameters.Generated.cs | 2 +- .../PluginRemoveParameters.Generated.cs | 2 +- .../Models/ServiceListParameters.Generated.cs | 4 +-- .../Models/ServiceLogsParameters.Generated.cs | 10 +++--- .../Models/SwarmLeaveParameters.Generated.cs | 2 +- .../Models/SwarmUpdateParameters.Generated.cs | 6 ++-- .../Models/TasksListParameters.Generated.cs | 2 +- .../Models/VolumesListParameters.Generated.cs | 2 +- .../VolumesPruneParameters.Generated.cs | 2 +- src/Docker.DotNet/QueryString.cs | 25 +------------ src/Docker.DotNet/QueryStringBoolConverter.cs | 15 -------- .../QueryStringBoolParameterAttribute.cs | 11 ++++++ .../QueryStringEnumerableConverter.cs | 36 ------------------- .../QueryStringListParameterAttribute.cs | 21 +++++++++++ src/Docker.DotNet/QueryStringMapConverter.cs | 16 --------- .../QueryStringMapParameterAttribute.cs | 12 +++++++ .../QueryStringParameterAttribute.cs | 10 +----- .../Docker.DotNet.SourceAnalyzers.csproj | 6 +++- .../JsonSerializerAnalyzer.cs | 14 ++++---- tools/specgen/specgen.go | 6 ++-- 42 files changed, 125 insertions(+), 185 deletions(-) delete mode 100644 src/Docker.DotNet/IQueryStringConverter.cs delete mode 100644 src/Docker.DotNet/QueryStringBoolConverter.cs create mode 100644 src/Docker.DotNet/QueryStringBoolParameterAttribute.cs delete mode 100644 src/Docker.DotNet/QueryStringEnumerableConverter.cs create mode 100644 src/Docker.DotNet/QueryStringListParameterAttribute.cs delete mode 100644 src/Docker.DotNet/QueryStringMapConverter.cs create mode 100644 src/Docker.DotNet/QueryStringMapParameterAttribute.cs diff --git a/src/Docker.DotNet/IQueryStringConverter.cs b/src/Docker.DotNet/IQueryStringConverter.cs deleted file mode 100644 index 4f07c50f..00000000 --- a/src/Docker.DotNet/IQueryStringConverter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Docker.DotNet; - -internal interface IQueryStringConverter -{ - bool CanConvert(Type t); - - string[] Convert(object o); -} \ No newline at end of file diff --git a/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs b/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs index 8c6df32e..3d841ec0 100644 --- a/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs +++ b/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs @@ -53,10 +53,10 @@ public CommitContainerChangesParameters(ContainerConfig Config) [QueryStringParameter("author", false)] public string? Author { get; set; } - [QueryStringParameter("changes", false)] + [QueryStringListParameter>("changes", false)] public IList? Changes { get; set; } - [QueryStringParameter("pause", false)] + [QueryStringBoolParameter("pause", false)] public bool? Pause { get; set; } [JsonPropertyName("Hostname")] diff --git a/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs index 64cfd5b4..5206f540 100644 --- a/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerAttachParameters.Generated.cs @@ -3,22 +3,22 @@ namespace Docker.DotNet.Models { public class ContainerAttachParameters // (main.ContainerAttachParameters) { - [QueryStringParameter("stream", false)] + [QueryStringBoolParameter("stream", false)] public bool? Stream { get; set; } - [QueryStringParameter("stdin", false)] + [QueryStringBoolParameter("stdin", false)] public bool? Stdin { get; set; } - [QueryStringParameter("stdout", false)] + [QueryStringBoolParameter("stdout", false)] public bool? Stdout { get; set; } - [QueryStringParameter("stderr", false)] + [QueryStringBoolParameter("stderr", false)] public bool? Stderr { get; set; } [QueryStringParameter("detachKeys", false)] public string? DetachKeys { get; set; } - [QueryStringParameter("logs", false)] + [QueryStringBoolParameter("logs", false)] public bool? Logs { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs index 703e8e1d..952db26a 100644 --- a/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerEventsParameters.Generated.cs @@ -9,7 +9,7 @@ public class ContainerEventsParameters // (main.ContainerEventsParameters) [QueryStringParameter("until", false)] public string? Until { get; set; } - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs index a92d682b..3f04667a 100644 --- a/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerInspectParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ContainerInspectParameters // (main.ContainerInspectParameters) { - [QueryStringParameter("size", false)] + [QueryStringBoolParameter("size", false)] public bool? IncludeSize { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs index 0809138c..f225c747 100644 --- a/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerLogsParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ContainerLogsParameters // (main.ContainerLogsParameters) { - [QueryStringParameter("stdout", false)] + [QueryStringBoolParameter("stdout", false)] public bool? ShowStdout { get; set; } - [QueryStringParameter("stderr", false)] + [QueryStringBoolParameter("stderr", false)] public bool? ShowStderr { get; set; } [QueryStringParameter("since", false)] @@ -15,10 +15,10 @@ public class ContainerLogsParameters // (main.ContainerLogsParameters) [QueryStringParameter("until", false)] public string? Until { get; set; } - [QueryStringParameter("timestamps", false)] + [QueryStringBoolParameter("timestamps", false)] public bool? Timestamps { get; set; } - [QueryStringParameter("follow", false)] + [QueryStringBoolParameter("follow", false)] public bool? Follow { get; set; } [QueryStringParameter("tail", false)] diff --git a/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs index ed1ca0f8..013de32e 100644 --- a/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerRemoveParameters.Generated.cs @@ -3,13 +3,13 @@ namespace Docker.DotNet.Models { public class ContainerRemoveParameters // (main.ContainerRemoveParameters) { - [QueryStringParameter("v", false)] + [QueryStringBoolParameter("v", false)] public bool? RemoveVolumes { get; set; } - [QueryStringParameter("link", false)] + [QueryStringBoolParameter("link", false)] public bool? RemoveLinks { get; set; } - [QueryStringParameter("force", false)] + [QueryStringBoolParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs b/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs index 97040d47..1a084a3b 100644 --- a/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainerStatsParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ContainerStatsParameters // (main.ContainerStatsParameters) { - [QueryStringParameter("stream", true)] + [QueryStringBoolParameter("stream", true)] public bool Stream { get; set; } = true; - [QueryStringParameter("one-shot", false)] + [QueryStringBoolParameter("one-shot", false)] public bool? OneShot { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs b/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs index 978d12a1..01d7b60a 100644 --- a/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainersListParameters.Generated.cs @@ -3,16 +3,16 @@ namespace Docker.DotNet.Models { public class ContainersListParameters // (main.ContainersListParameters) { - [QueryStringParameter("all", false)] + [QueryStringBoolParameter("all", false)] public bool? All { get; set; } [QueryStringParameter("limit", false)] public long? Limit { get; set; } - [QueryStringParameter("size", false)] + [QueryStringBoolParameter("size", false)] public bool? Size { get; set; } - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs b/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs index aea4404d..9cc67f0e 100644 --- a/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ContainersPruneParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ContainersPruneParameters // (main.ContainersPruneParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs b/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs index 4d76f72f..059d5497 100644 --- a/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs +++ b/src/Docker.DotNet/Models/CopyToContainerParameters.Generated.cs @@ -6,10 +6,10 @@ public class CopyToContainerParameters // (main.CopyToContainerParameters) [QueryStringParameter("path", true)] public string Path { get; set; } = default!; - [QueryStringParameter("noOverwriteDirNonDir", false)] + [QueryStringBoolParameter("noOverwriteDirNonDir", false)] public bool? AllowOverwriteDirWithFile { get; set; } - [QueryStringParameter("copyUIDGID", false)] + [QueryStringBoolParameter("copyUIDGID", false)] public bool? CopyUIDGID { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs b/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs index 2746c12d..4ad43896 100644 --- a/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs @@ -3,22 +3,22 @@ namespace Docker.DotNet.Models { public class ImageBuildParameters // (main.ImageBuildParameters) { - [QueryStringParameter("t", false)] + [QueryStringListParameter>("t", false)] public IList? Tags { get; set; } - [QueryStringParameter("q", false)] + [QueryStringBoolParameter("q", false)] public bool? SuppressOutput { get; set; } [QueryStringParameter("remote", false)] public string? RemoteContext { get; set; } - [QueryStringParameter("nocache", false)] + [QueryStringBoolParameter("nocache", false)] public bool? NoCache { get; set; } - [QueryStringParameter("rm", false)] + [QueryStringBoolParameter("rm", false)] public bool? Remove { get; set; } - [QueryStringParameter("forcerm", false)] + [QueryStringBoolParameter("forcerm", false)] public bool? ForceRemove { get; set; } [QueryStringParameter("pull", false)] @@ -51,19 +51,19 @@ public class ImageBuildParameters // (main.ImageBuildParameters) [QueryStringParameter("dockerfile", false)] public string? Dockerfile { get; set; } - [QueryStringParameter("buildargs", false)] + [QueryStringMapParameter>("buildargs", false)] public IDictionary? BuildArgs { get; set; } - [QueryStringParameter("labels", false)] + [QueryStringMapParameter>("labels", false)] public IDictionary? Labels { get; set; } - [QueryStringParameter("squash", false)] + [QueryStringBoolParameter("squash", false)] public bool? Squash { get; set; } - [QueryStringParameter("cachefrom", false)] + [QueryStringListParameter>("cachefrom", false)] public IList? CacheFrom { get; set; } - [QueryStringParameter("extrahosts", false)] + [QueryStringListParameter>("extrahosts", false)] public IList? ExtraHosts { get; set; } [QueryStringParameter("target", false)] diff --git a/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs b/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs index 54e224bf..fbb496c1 100644 --- a/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageDeleteParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ImageDeleteParameters // (main.ImageDeleteParameters) { - [QueryStringParameter("force", false)] + [QueryStringBoolParameter("force", false)] public bool? Force { get; set; } - [QueryStringParameter("noprune", false)] + [QueryStringBoolParameter("noprune", false)] public bool? NoPrune { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs b/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs index 09be1d2d..0f70d861 100644 --- a/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageLoadParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ImageLoadParameters // (main.ImageLoadParameters) { - [QueryStringParameter("quiet", true)] + [QueryStringBoolParameter("quiet", true)] public bool Quiet { get; set; } = default!; } } diff --git a/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs index 8d209899..1ce947b1 100644 --- a/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs @@ -18,7 +18,7 @@ public class ImagesCreateParameters // (main.ImagesCreateParameters) [QueryStringParameter("message", false)] public string? Message { get; set; } - [QueryStringParameter("changes", false)] + [QueryStringListParameter>("changes", false)] public IList? Changes { get; set; } [QueryStringParameter("platform", false)] diff --git a/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs index ba16676f..1b0a3ee5 100644 --- a/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesListParameters.Generated.cs @@ -3,19 +3,19 @@ namespace Docker.DotNet.Models { public class ImagesListParameters // (main.ImagesListParameters) { - [QueryStringParameter("all", false)] + [QueryStringBoolParameter("all", false)] public bool? All { get; set; } - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } - [QueryStringParameter("shared-size", false)] + [QueryStringBoolParameter("shared-size", false)] public bool? SharedSize { get; set; } - [QueryStringParameter("digests", false)] + [QueryStringBoolParameter("digests", false)] public bool? Digests { get; set; } - [QueryStringParameter("manifests", false)] + [QueryStringBoolParameter("manifests", false)] public bool? Manifests { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs index 6b9aca92..58bb5bc8 100644 --- a/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesPruneParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ImagesPruneParameters // (main.ImagesPruneParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs index 7713587a..338a41f8 100644 --- a/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesSearchParameters.Generated.cs @@ -9,7 +9,7 @@ public class ImagesSearchParameters // (main.ImagesSearchParameters) [QueryStringParameter("limit", false)] public long? Limit { get; set; } - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs b/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs index 2f47562c..968a4368 100644 --- a/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs +++ b/src/Docker.DotNet/Models/NetworksDeleteUnusedParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class NetworksDeleteUnusedParameters // (main.NetworksDeleteUnusedParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs b/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs index fcf031e7..aa0a0291 100644 --- a/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/NetworksListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class NetworksListParameters // (main.NetworksListParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs b/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs index 771b13ab..7356ccb3 100644 --- a/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/NodeRemoveParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class NodeRemoveParameters // (main.NodeRemoveParameters) { - [QueryStringParameter("force", false)] + [QueryStringBoolParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs b/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs index bc4f6d39..c6b14f74 100644 --- a/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs +++ b/src/Docker.DotNet/Models/PluginDisableParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class PluginDisableParameters // (main.PluginDisableParameters) { - [QueryStringParameter("force", false)] + [QueryStringBoolParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/PluginListParameters.Generated.cs b/src/Docker.DotNet/Models/PluginListParameters.Generated.cs index 66133967..8afee824 100644 --- a/src/Docker.DotNet/Models/PluginListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/PluginListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class PluginListParameters // (main.PluginListParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs b/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs index 9477c396..3e426c5f 100644 --- a/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/PluginRemoveParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class PluginRemoveParameters // (main.PluginRemoveParameters) { - [QueryStringParameter("force", false)] + [QueryStringBoolParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs b/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs index 9ab1981f..be6a52b3 100644 --- a/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ServiceListParameters.Generated.cs @@ -3,10 +3,10 @@ namespace Docker.DotNet.Models { public class ServiceListParameters // (main.ServiceListParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } - [QueryStringParameter("status", false)] + [QueryStringBoolParameter("status", false)] public bool? Status { get; set; } } } diff --git a/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs b/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs index ea652f20..2e1644e8 100644 --- a/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ServiceLogsParameters.Generated.cs @@ -3,25 +3,25 @@ namespace Docker.DotNet.Models { public class ServiceLogsParameters // (main.ServiceLogsParameters) { - [QueryStringParameter("stdout", false)] + [QueryStringBoolParameter("stdout", false)] public bool? ShowStdout { get; set; } - [QueryStringParameter("stderr", false)] + [QueryStringBoolParameter("stderr", false)] public bool? ShowStderr { get; set; } [QueryStringParameter("since", false)] public string? Since { get; set; } - [QueryStringParameter("timestamps", false)] + [QueryStringBoolParameter("timestamps", false)] public bool? Timestamps { get; set; } - [QueryStringParameter("follow", false)] + [QueryStringBoolParameter("follow", false)] public bool? Follow { get; set; } [QueryStringParameter("tail", false)] public string? Tail { get; set; } - [QueryStringParameter("details", false)] + [QueryStringBoolParameter("details", false)] public bool? Details { get; set; } } } diff --git a/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs b/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs index bb1d9041..cc0a381f 100644 --- a/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs +++ b/src/Docker.DotNet/Models/SwarmLeaveParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class SwarmLeaveParameters // (main.SwarmLeaveParameters) { - [QueryStringParameter("force", false)] + [QueryStringBoolParameter("force", false)] public bool? Force { get; set; } } } diff --git a/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs b/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs index 5223887b..0b1daf7a 100644 --- a/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs +++ b/src/Docker.DotNet/Models/SwarmUpdateParameters.Generated.cs @@ -9,13 +9,13 @@ public class SwarmUpdateParameters // (main.SwarmUpdateParameters) [QueryStringParameter("version", true)] public long Version { get; set; } = default!; - [QueryStringParameter("rotateworkertoken", false)] + [QueryStringBoolParameter("rotateworkertoken", false)] public bool? RotateWorkerToken { get; set; } - [QueryStringParameter("rotatemanagertoken", false)] + [QueryStringBoolParameter("rotatemanagertoken", false)] public bool? RotateManagerToken { get; set; } - [QueryStringParameter("rotatemanagerunlockkey", false)] + [QueryStringBoolParameter("rotatemanagerunlockkey", false)] public bool? RotateManagerUnlockKey { get; set; } } } diff --git a/src/Docker.DotNet/Models/TasksListParameters.Generated.cs b/src/Docker.DotNet/Models/TasksListParameters.Generated.cs index ba967ef6..6a8d5f9c 100644 --- a/src/Docker.DotNet/Models/TasksListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/TasksListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class TasksListParameters // (main.TasksListParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs b/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs index 64f5de68..ebace0c1 100644 --- a/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs +++ b/src/Docker.DotNet/Models/VolumesListParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class VolumesListParameters // (main.VolumesListParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs b/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs index 9c0d2abd..b787cd8b 100644 --- a/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs +++ b/src/Docker.DotNet/Models/VolumesPruneParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class VolumesPruneParameters // (main.VolumesPruneParameters) { - [QueryStringParameter("filters", false)] + [QueryStringMapParameter>>("filters", false)] public IDictionary>? Filters { get; set; } } } diff --git a/src/Docker.DotNet/QueryString.cs b/src/Docker.DotNet/QueryString.cs index 9a74e019..74323ef9 100644 --- a/src/Docker.DotNet/QueryString.cs +++ b/src/Docker.DotNet/QueryString.cs @@ -41,20 +41,7 @@ public IDictionary GetKeyValuePairs() if (attribute.IsRequired || !IsDefaultOfType(value)) { var keyStr = attribute.Name; - string[] valueStr; - if (attribute.GetConverter() is { } converter) - { - valueStr = ConvertValue(converter, value!); - - if (valueStr == null) - { - throw new InvalidOperationException($"Got null from value converter '{converter.GetType().FullName}'"); - } - } - else - { - valueStr = [value!.ToString()!]; - } + var valueStr = attribute.Convert(value!); queryParameters[keyStr] = valueStr; } @@ -76,16 +63,6 @@ public string GetQueryString() v => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(v)}")))); } - private static string[] ConvertValue(IQueryStringConverter converter, object value) - { - if (!converter.CanConvert(value.GetType())) - { - throw new InvalidOperationException( - $"Cannot convert type {value.GetType().FullName} using {converter.GetType().FullName}."); - } - return converter.Convert(value); - } - private static Dictionary FindAttributedPublicProperties< #if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/Docker.DotNet/QueryStringBoolConverter.cs b/src/Docker.DotNet/QueryStringBoolConverter.cs deleted file mode 100644 index 7ff3a44d..00000000 --- a/src/Docker.DotNet/QueryStringBoolConverter.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Docker.DotNet; - -internal class QueryStringBoolConverter : IQueryStringConverter -{ - public bool CanConvert(Type t) - { - return t == typeof (bool); - } - - public string[] Convert(object o) - { - Debug.Assert(o != null); - return new[] {System.Convert.ToInt32(System.Convert.ToBoolean(o)).ToString(CultureInfo.InvariantCulture)}; - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs b/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs new file mode 100644 index 00000000..9135aa8d --- /dev/null +++ b/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs @@ -0,0 +1,11 @@ +namespace Docker.DotNet; + +internal sealed class QueryStringBoolParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) +{ + public override string[] Convert(object value) + { + Debug.Assert(value != null); + + return [System.Convert.ToInt32(System.Convert.ToBoolean(value)).ToString(CultureInfo.InvariantCulture)]; + } +} diff --git a/src/Docker.DotNet/QueryStringEnumerableConverter.cs b/src/Docker.DotNet/QueryStringEnumerableConverter.cs deleted file mode 100644 index 7d21a955..00000000 --- a/src/Docker.DotNet/QueryStringEnumerableConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Docker.DotNet; - -/// -/// Handles serialization of objects like Lists, Arrays, etc. -/// -internal class QueryStringEnumerableConverter : IQueryStringConverter -{ - public bool CanConvert(Type t) - { - return typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()); - } - - public string[] Convert(object o) - { - Debug.Assert(o != null); - Debug.Assert(o is IEnumerable); - - var enumerable = (IEnumerable)o!; - - var items = new List(); - - foreach (var e in enumerable) - { - if (e is string or ValueType) - { - items.Add(e.ToString()!); - } - else - { - items.Add(JsonSerializer.Instance.Serialize(e)); - } - } - - return items.ToArray(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringListParameterAttribute.cs b/src/Docker.DotNet/QueryStringListParameterAttribute.cs new file mode 100644 index 00000000..d4d907dd --- /dev/null +++ b/src/Docker.DotNet/QueryStringListParameterAttribute.cs @@ -0,0 +1,21 @@ +namespace Docker.DotNet; + +internal sealed class QueryStringListParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) where T : IList +{ + public override string[] Convert(object value) + { + Debug.Assert(value != null); + Debug.Assert(value is IList); + + var enumerable = (IList)value!; + + var items = new List(); + + foreach (var e in enumerable) + { + items.Add(e.ToString()!); + } + + return items.ToArray(); + } +} diff --git a/src/Docker.DotNet/QueryStringMapConverter.cs b/src/Docker.DotNet/QueryStringMapConverter.cs deleted file mode 100644 index 5a9826e1..00000000 --- a/src/Docker.DotNet/QueryStringMapConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Docker.DotNet; - -internal class QueryStringMapConverter : IQueryStringConverter -{ - public bool CanConvert(Type t) - { - return typeof(IList).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) || typeof(IDictionary).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()); - } - - public string[] Convert(object o) - { - Debug.Assert(o != null); - - return new[] { JsonSerializer.Instance.Serialize(o) }; - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringMapParameterAttribute.cs b/src/Docker.DotNet/QueryStringMapParameterAttribute.cs new file mode 100644 index 00000000..50298420 --- /dev/null +++ b/src/Docker.DotNet/QueryStringMapParameterAttribute.cs @@ -0,0 +1,12 @@ +namespace Docker.DotNet; + +internal sealed class QueryStringMapParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) +{ + public override string[] Convert(object value) + { + Debug.Assert(value != null); + Debug.Assert(value is T); + + return [JsonSerializer.Instance.Serialize((T)value!)]; + } +} diff --git a/src/Docker.DotNet/QueryStringParameterAttribute.cs b/src/Docker.DotNet/QueryStringParameterAttribute.cs index 8e73e463..cc4e6364 100644 --- a/src/Docker.DotNet/QueryStringParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringParameterAttribute.cs @@ -7,7 +7,7 @@ internal class QueryStringParameterAttribute : Attribute public bool IsRequired { get; private set; } - public virtual IQueryStringConverter? GetConverter() => null; + public virtual string[] Convert(object value) => [value.ToString()!]; public QueryStringParameterAttribute(string name, bool required) { @@ -20,11 +20,3 @@ public QueryStringParameterAttribute(string name, bool required) IsRequired = required; } } - -internal sealed class QueryStringParameterAttribute(string name, bool required) - : QueryStringParameterAttribute(name, required) where TConverter : IQueryStringConverter, new() -{ - private static TConverter? _converterInstance; - - public override IQueryStringConverter GetConverter() => _converterInstance ??= new TConverter(); -} \ No newline at end of file diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj index 3c329327..a95253a1 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -1,10 +1,14 @@  - netstandard2.0;net8.0 + net8.0;net9.0;net10.0;netstandard2.0 true false false + + enable + $(NoWarn);RS1041;RS2008 + all diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs index 5eb40e21..c4c80bec 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -19,10 +19,10 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - // Handle method calls (MakeRequestAsync, etc.) + // Handle method calls (MakeRequestAsync, MonitorStreamForMessagesAsync and all methods call to JsonSerializer.*) context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); - // Handle 'new JsonRequestContent' + // Handle new JsonRequestContent context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); } @@ -38,9 +38,9 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) (methodSymbol.Name == "MonitorStreamForMessagesAsync" && methodSymbol.ContainingType?.Name == "StreamUtil") || (methodSymbol.ContainingType?.Name == "JsonSerializer" && methodSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet"); - if (isStjEntryPoint && methodSymbol.TypeArguments.Length == 1) + if (isStjEntryPoint && methodSymbol.TypeArguments.Length > 0) { - var methodName = $"{methodSymbol.ContainingType.Name}.{methodSymbol.Name}"; + var methodName = $"{methodSymbol.ContainingType!.Name}.{methodSymbol.Name}"; CheckAndReportType(context, methodSymbol.TypeArguments[0], invocation.GetLocation(), methodName); } } @@ -54,11 +54,10 @@ private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) var typeSymbol = constructorSymbol.ContainingType; - // Check if we are instantiating JsonRequestContent if (typeSymbol.Name == "JsonRequestContent" && typeSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet") { - if (typeSymbol.TypeArguments.Length == 1) + if (typeSymbol.TypeArguments.Length > 0) { var typeName = typeSymbol.Name; CheckAndReportType(context, typeSymbol.TypeArguments[0], creation.GetLocation(), typeName); @@ -68,9 +67,8 @@ private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) private static void CheckAndReportType(SyntaxNodeAnalysisContext context, ITypeSymbol targetType, Location location, string sourceName) { - // Skip generic type parameters, Object, and the internal NoContent marker + // Skip generic type parameters and the internal NoContent marker if (targetType.TypeKind == TypeKind.TypeParameter || - targetType.SpecialType == SpecialType.System_Object || targetType.Name == "NoContent") { return; diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index 848f5f18..a9600a95 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -698,11 +698,11 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { switch f.Type.Kind() { case reflect.Bool: - queryStringParameter += "" + queryStringParameter = "QueryStringBoolParameter" case reflect.Slice, reflect.Array: - queryStringParameter += "" + queryStringParameter = "QueryStringListParameter<" + csProp.Type.Name + ">" case reflect.Map: - queryStringParameter += "" + queryStringParameter = "QueryStringMapParameter<" + csProp.Type.Name + ">" } a := CSAttribute{Type: CSType{"", queryStringParameter}} From 3f275c0184e687ba9912be6e9cf29e963d4fafcf Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:03:22 +0200 Subject: [PATCH 17/24] chore: improve query string parameter attribute implementations --- .../CommitContainerChangesParameters.Generated.cs | 2 +- .../Models/ImageBuildParameters.Generated.cs | 6 +++--- .../Models/ImagesCreateParameters.Generated.cs | 2 +- .../QueryStringBoolParameterAttribute.cs | 2 +- .../QueryStringListParameterAttribute.cs | 14 +++++--------- .../QueryStringMapParameterAttribute.cs | 9 +++++++-- src/Docker.DotNet/QueryStringParameterAttribute.cs | 2 +- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs b/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs index 3d841ec0..de6442ac 100644 --- a/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs +++ b/src/Docker.DotNet/Models/CommitContainerChangesParameters.Generated.cs @@ -53,7 +53,7 @@ public CommitContainerChangesParameters(ContainerConfig Config) [QueryStringParameter("author", false)] public string? Author { get; set; } - [QueryStringListParameter>("changes", false)] + [QueryStringListParameter("changes", false)] public IList? Changes { get; set; } [QueryStringBoolParameter("pause", false)] diff --git a/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs b/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs index 4ad43896..92e55b50 100644 --- a/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImageBuildParameters.Generated.cs @@ -3,7 +3,7 @@ namespace Docker.DotNet.Models { public class ImageBuildParameters // (main.ImageBuildParameters) { - [QueryStringListParameter>("t", false)] + [QueryStringListParameter("t", false)] public IList? Tags { get; set; } [QueryStringBoolParameter("q", false)] @@ -60,10 +60,10 @@ public class ImageBuildParameters // (main.ImageBuildParameters) [QueryStringBoolParameter("squash", false)] public bool? Squash { get; set; } - [QueryStringListParameter>("cachefrom", false)] + [QueryStringListParameter("cachefrom", false)] public IList? CacheFrom { get; set; } - [QueryStringListParameter>("extrahosts", false)] + [QueryStringListParameter("extrahosts", false)] public IList? ExtraHosts { get; set; } [QueryStringParameter("target", false)] diff --git a/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs b/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs index 1ce947b1..4cc7c51b 100644 --- a/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs +++ b/src/Docker.DotNet/Models/ImagesCreateParameters.Generated.cs @@ -18,7 +18,7 @@ public class ImagesCreateParameters // (main.ImagesCreateParameters) [QueryStringParameter("message", false)] public string? Message { get; set; } - [QueryStringListParameter>("changes", false)] + [QueryStringListParameter("changes", false)] public IList? Changes { get; set; } [QueryStringParameter("platform", false)] diff --git a/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs b/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs index 9135aa8d..368b497f 100644 --- a/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringBoolParameterAttribute.cs @@ -8,4 +8,4 @@ public override string[] Convert(object value) return [System.Convert.ToInt32(System.Convert.ToBoolean(value)).ToString(CultureInfo.InvariantCulture)]; } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringListParameterAttribute.cs b/src/Docker.DotNet/QueryStringListParameterAttribute.cs index d4d907dd..16e130f4 100644 --- a/src/Docker.DotNet/QueryStringListParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringListParameterAttribute.cs @@ -1,21 +1,17 @@ namespace Docker.DotNet; -internal sealed class QueryStringListParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) where T : IList +internal sealed class QueryStringListParameterAttribute(string name, bool required) : QueryStringParameterAttribute(name, required) { public override string[] Convert(object value) { Debug.Assert(value != null); Debug.Assert(value is IList); - var enumerable = (IList)value!; - - var items = new List(); - - foreach (var e in enumerable) + if (value is not IList typedValue) { - items.Add(e.ToString()!); + throw new ArgumentException($"Expected value of type '{typeof(IList)}'.", nameof(value)); } - return items.ToArray(); + return typedValue.ToArray(); } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringMapParameterAttribute.cs b/src/Docker.DotNet/QueryStringMapParameterAttribute.cs index 50298420..c8fc3933 100644 --- a/src/Docker.DotNet/QueryStringMapParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringMapParameterAttribute.cs @@ -7,6 +7,11 @@ public override string[] Convert(object value) Debug.Assert(value != null); Debug.Assert(value is T); - return [JsonSerializer.Instance.Serialize((T)value!)]; + if (value is not T typedValue) + { + throw new ArgumentException($"Expected value of type '{typeof(T)}'.", nameof(value)); + } + + return [JsonSerializer.Instance.Serialize(typedValue)]; } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet/QueryStringParameterAttribute.cs b/src/Docker.DotNet/QueryStringParameterAttribute.cs index cc4e6364..11032a6b 100644 --- a/src/Docker.DotNet/QueryStringParameterAttribute.cs +++ b/src/Docker.DotNet/QueryStringParameterAttribute.cs @@ -19,4 +19,4 @@ public QueryStringParameterAttribute(string name, bool required) Name = name; IsRequired = required; } -} +} \ No newline at end of file From 497aac58813fab6726cb4ccece50dfced4216777 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:52:48 +0200 Subject: [PATCH 18/24] chore: remove nullable warning --- src/Docker.DotNet/Endpoints/ImageOperations.cs | 2 +- src/Microsoft.Net.Http.Client/HttpConnection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ImageOperations.cs b/src/Docker.DotNet/Endpoints/ImageOperations.cs index 87e0b068..304c7a94 100644 --- a/src/Docker.DotNet/Endpoints/ImageOperations.cs +++ b/src/Docker.DotNet/Endpoints/ImageOperations.cs @@ -313,7 +313,7 @@ private static Dictionary RegistryConfigHeaders(IEnumerable 0) { registryAuthConfigurations[registryAddress] = registryAuthConfiguration; } diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 7cf88c49..85ad4b1f 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -121,7 +121,7 @@ private async Task> ReadResponseLinesAsync(CancellationToken cancel var line = await Transport.ReadLineAsync(cancellationToken) .ConfigureAwait(false); - if (string.IsNullOrEmpty(line)) + if (line is null || line.Length == 0) { break; } From bbc29e8225e5360e3d41bea52a94dab97e60bbb4 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:53:19 +0200 Subject: [PATCH 19/24] chore: clean up analyzer --- .../Docker.DotNet.SourceAnalyzers.csproj | 10 ++-- .../JsonSerializerAnalyzer.cs | 59 ++++++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj index a95253a1..d9ddd997 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -1,13 +1,13 @@  - net8.0;net9.0;net10.0;netstandard2.0 + netstandard2.0 true false false enable - $(NoWarn);RS1041;RS2008 + $(NoWarn);RS2008 @@ -17,14 +17,14 @@ + + - - - + diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs index c4c80bec..e328b2cd 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -3,14 +3,15 @@ [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class JsonSerializerAnalyzer : DiagnosticAnalyzer { - public const string DiagnosticId = "DDN001"; + private const string DiagnosticId = "DDN001"; + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, "Missing JsonSerializable attribute", - "Type '{0}' used in {1} is not registered in DockerExtendedJsonSerializerContext", + "Type '{0}' used in '{1}' is not registered in a Docker JSON serializer context", "Usage", DiagnosticSeverity.Error, - isEnabledByDefault: true); + true); public override ImmutableArray SupportedDiagnostics => [Rule]; @@ -19,10 +20,10 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - // Handle method calls (MakeRequestAsync, MonitorStreamForMessagesAsync and all methods call to JsonSerializer.*) + // Handle method calls (MakeRequestAsync, MonitorStreamForMessagesAsync and all methods call of JsonSerializer.*). context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); - // Handle new JsonRequestContent + // Handle new JsonRequestContent. context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); } @@ -31,9 +32,11 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) var invocation = (InvocationExpressionSyntax)context.Node; if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) + { return; + } - bool isStjEntryPoint = + var isStjEntryPoint = (methodSymbol.Name == "MakeRequestAsync" && methodSymbol.ContainingType?.Name == "DockerClient") || (methodSymbol.Name == "MonitorStreamForMessagesAsync" && methodSymbol.ContainingType?.Name == "StreamUtil") || (methodSymbol.ContainingType?.Name == "JsonSerializer" && methodSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet"); @@ -50,24 +53,26 @@ private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) var creation = (ObjectCreationExpressionSyntax)context.Node; if (context.SemanticModel.GetSymbolInfo(creation).Symbol is not IMethodSymbol constructorSymbol) + { return; + } var typeSymbol = constructorSymbol.ContainingType; - if (typeSymbol.Name == "JsonRequestContent" && - typeSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet") + var isStjEntryPoint = + typeSymbol.Name == "JsonRequestContent" && + typeSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet"; + + if (isStjEntryPoint && typeSymbol.TypeArguments.Length > 0) { - if (typeSymbol.TypeArguments.Length > 0) - { - var typeName = typeSymbol.Name; - CheckAndReportType(context, typeSymbol.TypeArguments[0], creation.GetLocation(), typeName); - } + var typeName = typeSymbol.Name; + CheckAndReportType(context, typeSymbol.TypeArguments[0], creation.GetLocation(), typeName); } } private static void CheckAndReportType(SyntaxNodeAnalysisContext context, ITypeSymbol targetType, Location location, string sourceName) { - // Skip generic type parameters and the internal NoContent marker + // Skip generic type parameters and the internal NoContent marker. if (targetType.TypeKind == TypeKind.TypeParameter || targetType.Name == "NoContent") { @@ -75,24 +80,34 @@ private static void CheckAndReportType(SyntaxNodeAnalysisContext context, ITypeS } var jsonSerializableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute"); - if (jsonSerializableAttributeTypeSymbol == null) return; + if (jsonSerializableAttributeTypeSymbol == null) + { + return; + } - var isRegistered = false; var jsonSerializerContextNames = new[] { "Docker.DotNet.DockerExtendedJsonSerializerContext", "Docker.DotNet.Models.DockerModelsJsonSerializerContext" }; + var isRegistered = false; + foreach (var jsonSerializerContextName in jsonSerializerContextNames) { var jsonSerializerContextTypeSymbol = context.Compilation.GetTypeByMetadataName(jsonSerializerContextName); - if (jsonSerializerContextTypeSymbol == null) continue; + if (jsonSerializerContextTypeSymbol == null) + { + continue; + } + + var isRegisteredInContext = + jsonSerializerContextTypeSymbol.GetAttributes().Any(attr => + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonSerializableAttributeTypeSymbol) && + attr.ConstructorArguments.Any(arg => + arg.Value is ITypeSymbol reg && SymbolEqualityComparer.Default.Equals(reg, targetType))); - if (jsonSerializerContextTypeSymbol.GetAttributes().Any(attr => - SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonSerializableAttributeTypeSymbol) && - attr.ConstructorArguments.Any(arg => - arg.Value is ITypeSymbol reg && SymbolEqualityComparer.Default.Equals(reg, targetType)))) + if (isRegisteredInContext) { isRegistered = true; break; @@ -104,4 +119,4 @@ private static void CheckAndReportType(SyntaxNodeAnalysisContext context, ITypeS context.ReportDiagnostic(Diagnostic.Create(Rule, location, targetType.ToDisplayString(), sourceName)); } } -} +} \ No newline at end of file From 1f7f9236b676f18c3fce9db4deb9b0fdacdcf140 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:53:54 +0200 Subject: [PATCH 20/24] chore: remove BOM --- .../Docker.DotNet.SourceAnalyzers.csproj | 2 +- tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj index d9ddd997..095006b3 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 true diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs index e328b2cd..81239561 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet.SourceAnalyzers; +namespace Docker.DotNet.SourceAnalyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class JsonSerializerAnalyzer : DiagnosticAnalyzer From c1672a59ca19f417f401befdece438c992593c91 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:56:44 +0200 Subject: [PATCH 21/24] chore: group order serializer context --- src/Docker.DotNet/JsonSerializer.cs | 70 +++++++++++++---------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/Docker.DotNet/JsonSerializer.cs b/src/Docker.DotNet/JsonSerializer.cs index 7e03d43f..ccb60272 100644 --- a/src/Docker.DotNet/JsonSerializer.cs +++ b/src/Docker.DotNet/JsonSerializer.cs @@ -116,43 +116,35 @@ private static class JsonTypeInfoCache [JsonSerializable(typeof(IDictionary>))] [JsonSerializable(typeof(IDictionary))] -// ConfigOperations.ListConfigsAsync -[JsonSerializable(typeof(SwarmConfig[]))] - -// ContainerOperations.ListContainersAsync -[JsonSerializable(typeof(ContainerListResponse[]))] -// ContainerOperations.InspectChangesAsync -[JsonSerializable(typeof(ContainerFileSystemChangeResponse[]))] - -// ImageOperations.ListImagesAsync -[JsonSerializable(typeof(ImagesListResponse[]))] -// ImageOperations.GetImageHistoryAsync -[JsonSerializable(typeof(ImageHistoryResponse[]))] -// ImageOperations.DeleteImageAsync -[JsonSerializable(typeof(Dictionary[]))] -// ImageOperations.SearchImagesAsync -[JsonSerializable(typeof(ImageSearchResponse[]))] -// ImageOperations.RegistryConfigHeaders -[JsonSerializable(typeof(Dictionary))] - -// NetworkOperations.ListNetworksAsync -[JsonSerializable(typeof(NetworkResponse[]))] - -// PluginOperations.ListPluginsAsync -[JsonSerializable(typeof(Plugin[]))] -// PluginOperations.GetPrivilegesAsync -[JsonSerializable(typeof(PluginPrivilege[]))] -// PluginOperations.InstallPluginAsync -[JsonSerializable(typeof(IList))] - -// SecretOperations.ListAsync -[JsonSerializable(typeof(Secret[]))] - -// SwarmOperations.ListServicesAsync -[JsonSerializable(typeof(SwarmService[]))] -// SwarmOperations.ListNodesAsync -[JsonSerializable(typeof(NodeListResponse[]))] - -// TaskOperations.ListAsync -[JsonSerializable(typeof(TaskResponse[]))] +// ConfigOperations +[JsonSerializable(typeof(SwarmConfig[]))] // ListConfigsAsync + +// ContainerOperations +[JsonSerializable(typeof(ContainerFileSystemChangeResponse[]))] // InspectChangesAsync +[JsonSerializable(typeof(ContainerListResponse[]))] // ListContainersAsync + +// ImageOperations +[JsonSerializable(typeof(Dictionary[]))] // DeleteImageAsync +[JsonSerializable(typeof(ImageHistoryResponse[]))] // GetImageHistoryAsync +[JsonSerializable(typeof(ImagesListResponse[]))] // ListImagesAsync +[JsonSerializable(typeof(Dictionary))] // RegistryConfigHeaders +[JsonSerializable(typeof(ImageSearchResponse[]))] // SearchImagesAsync + +// NetworkOperations +[JsonSerializable(typeof(NetworkResponse[]))] // ListNetworksAsync + +// PluginOperations +[JsonSerializable(typeof(PluginPrivilege[]))] // GetPrivilegesAsync +[JsonSerializable(typeof(IList))] // InstallPluginAsync +[JsonSerializable(typeof(Plugin[]))] // ListPluginsAsync + +// SecretOperations +[JsonSerializable(typeof(Secret[]))] // ListAsync + +// SwarmOperations +[JsonSerializable(typeof(NodeListResponse[]))] // ListNodesAsync +[JsonSerializable(typeof(SwarmService[]))] // ListServicesAsync + +// TaskOperations +[JsonSerializable(typeof(TaskResponse[]))] // ListAsync internal sealed partial class DockerExtendedJsonSerializerContext : JsonSerializerContext { } From dd5f7446865c98533bd5ac03502fd3a49e87b39a Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:12:01 +0200 Subject: [PATCH 22/24] feat: analyze QueryStringMapParameterAttribute --- .../JsonSerializerAnalyzer.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs index 81239561..cdce4538 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs +++ b/tools/Docker.DotNet.SourceAnalyzers/JsonSerializerAnalyzer.cs @@ -17,7 +17,8 @@ public sealed class JsonSerializerAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); // Handle method calls (MakeRequestAsync, MonitorStreamForMessagesAsync and all methods call of JsonSerializer.*). @@ -25,6 +26,9 @@ public override void Initialize(AnalysisContext context) // Handle new JsonRequestContent. context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); + + // Handle [QueryStringMapParameter(...)]. + context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute); } private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) @@ -70,6 +74,27 @@ private static void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) } } + private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context) + { + var attribute = (AttributeSyntax)context.Node; + + if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol constructorSymbol) + { + return; + } + + var typeSymbol = constructorSymbol.ContainingType; + + var isStjEntryPoint = + typeSymbol.Name == "QueryStringMapParameterAttribute" && + typeSymbol.ContainingNamespace.ToDisplayString() == "Docker.DotNet"; + + if (isStjEntryPoint && typeSymbol.TypeArguments.Length > 0) + { + CheckAndReportType(context, typeSymbol.TypeArguments[0], attribute.GetLocation(), typeSymbol.Name); + } + } + private static void CheckAndReportType(SyntaxNodeAnalysisContext context, ITypeSymbol targetType, Location location, string sourceName) { // Skip generic type parameters and the internal NoContent marker. From 1c23a047d3a5b616be4c8ee91c114da566b2ab40 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:38:20 +0200 Subject: [PATCH 23/24] fix: add multi TFM --- .../Docker.DotNet.SourceAnalyzers.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj index 095006b3..9575123c 100644 --- a/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj +++ b/tools/Docker.DotNet.SourceAnalyzers/Docker.DotNet.SourceAnalyzers.csproj @@ -1,13 +1,13 @@ - netstandard2.0 + net8.0;net9.0;net10.0;netstandard2.0 true false false enable - $(NoWarn);RS2008 + $(NoWarn);RS1041;RS2008 From da83de13c38147da8f0e6aee7ed4c0d5668758c7 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:42:41 +0200 Subject: [PATCH 24/24] fix: use QueryStringListParameter in specgen --- tools/specgen/specgen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index a9600a95..2fa31c59 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -700,7 +700,7 @@ func reflectTypeMembers(t reflect.Type, m *CSModelType) { case reflect.Bool: queryStringParameter = "QueryStringBoolParameter" case reflect.Slice, reflect.Array: - queryStringParameter = "QueryStringListParameter<" + csProp.Type.Name + ">" + queryStringParameter = "QueryStringListParameter" case reflect.Map: queryStringParameter = "QueryStringMapParameter<" + csProp.Type.Name + ">" }