diff --git a/examples/prometheus/Prometheus.cs b/examples/prometheus/Prometheus.cs index 49239fc..fc7baa7 100755 --- a/examples/prometheus/Prometheus.cs +++ b/examples/prometheus/Prometheus.cs @@ -1,7 +1,6 @@ using k8s; using Prometheus; using System; -using System.Net.Http; using System.Threading; namespace prom @@ -12,7 +11,7 @@ namespace prom { var config = KubernetesClientConfiguration.BuildDefaultConfig(); var handler = new PrometheusHandler(); - IKubernetes client = new Kubernetes(config, new DelegatingHandler[] { handler }); + IKubernetes client = new Kubernetes(config, handler); var server = new MetricServer(hostname: "localhost", port: 1234); server.Start(); diff --git a/src/KubernetesClient.Models/KubernetesJson.cs b/src/KubernetesClient.Models/KubernetesJson.cs index ff22037..70709c6 100644 --- a/src/KubernetesClient.Models/KubernetesJson.cs +++ b/src/KubernetesClient.Models/KubernetesJson.cs @@ -66,20 +66,36 @@ namespace k8s JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } - public static TValue Deserialize(string json) + /// + /// Configures for the . + /// To override existing converters, add them to the top of the list + /// e.g. as follows: options.Converters.Insert(index: 0, new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + /// + /// An to configure the . + public static void AddJsonOptions(Action configure) { - return JsonSerializer.Deserialize(json, JsonSerializerOptions); + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } + + configure(JsonSerializerOptions); } - public static TValue Deserialize(Stream json) + public static TValue Deserialize(string json, JsonSerializerOptions jsonSerializerOptions = null) { - return JsonSerializer.Deserialize(json, JsonSerializerOptions); + return JsonSerializer.Deserialize(json, jsonSerializerOptions ?? JsonSerializerOptions); + } + + public static TValue Deserialize(Stream json, JsonSerializerOptions jsonSerializerOptions = null) + { + return JsonSerializer.Deserialize(json, jsonSerializerOptions ?? JsonSerializerOptions); } - public static string Serialize(object value) + public static string Serialize(object value, JsonSerializerOptions jsonSerializerOptions = null) { - return JsonSerializer.Serialize(value, JsonSerializerOptions); + return JsonSerializer.Serialize(value, jsonSerializerOptions ?? JsonSerializerOptions); } } } diff --git a/src/KubernetesClient/Kubernetes.ConfigInit.cs b/src/KubernetesClient/Kubernetes.ConfigInit.cs index f8f504c..762b61f 100644 --- a/src/KubernetesClient/Kubernetes.ConfigInit.cs +++ b/src/KubernetesClient/Kubernetes.ConfigInit.cs @@ -9,6 +9,8 @@ namespace k8s { public partial class Kubernetes { + private readonly JsonSerializerOptions jsonSerializerOptions; + /// /// Initializes a new instance of the class. /// @@ -27,6 +29,7 @@ namespace k8s CreateHttpClient(handlers, config); InitializeFromConfig(config); HttpClientTimeout = config.HttpClientTimeout; + jsonSerializerOptions = config.JsonSerializerOptions; #if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER DisableHttp2 = config.DisableHttp2; #endif @@ -155,8 +158,6 @@ namespace k8s }; } - - /// /// Set credentials for the Client /// diff --git a/src/KubernetesClient/Kubernetes.cs b/src/KubernetesClient/Kubernetes.cs index 848ac49..bbbe70e 100644 --- a/src/KubernetesClient/Kubernetes.cs +++ b/src/KubernetesClient/Kubernetes.cs @@ -87,7 +87,7 @@ namespace k8s using (Stream stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false)) #endif { - result.Body = KubernetesJson.Deserialize(stream); + result.Body = KubernetesJson.Deserialize(stream, jsonSerializerOptions); } } catch (JsonException) @@ -126,7 +126,7 @@ namespace k8s if (body != null) { - var requestContent = KubernetesJson.Serialize(body); + var requestContent = KubernetesJson.Serialize(body, jsonSerializerOptions); httpRequest.Content = new StringContent(requestContent, System.Text.Encoding.UTF8); httpRequest.Content.Headers.ContentType = GetHeader(body); return SendRequestRaw(requestContent, httpRequest, cancellationToken); diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs index 3dafebb..a1cf397 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs @@ -524,7 +524,7 @@ namespace k8s process.StartInfo.RedirectStandardError = true; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; - + return process; } diff --git a/src/KubernetesClient/KubernetesClientConfiguration.cs b/src/KubernetesClient/KubernetesClientConfiguration.cs index b6e2f8d..61e0d06 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.cs @@ -9,6 +9,8 @@ namespace k8s /// public partial class KubernetesClientConfiguration { + private JsonSerializerOptions jsonSerializerOptions; + /// /// Gets current namespace /// @@ -108,5 +110,38 @@ namespace k8s /// Do not use http2 even it is available /// public bool DisableHttp2 { get; set; } = false; + + /// + /// Options for the to override the default ones. + /// + public JsonSerializerOptions JsonSerializerOptions + { + get + { + // If not yet set, use defaults from KubernetesJson. + if (jsonSerializerOptions is null) + { + KubernetesJson.AddJsonOptions(options => + { + jsonSerializerOptions = new JsonSerializerOptions(options); + }); + } + + return jsonSerializerOptions; + } + + private set => jsonSerializerOptions = value; + } + + /// + public void AddJsonOptions(Action configure) + { + if (configure is null) + { + throw new ArgumentNullException(nameof(configure)); + } + + configure(JsonSerializerOptions); + } } } diff --git a/tests/KubernetesClient.Tests/SerializationTests.cs b/tests/KubernetesClient.Tests/SerializationTests.cs new file mode 100644 index 0000000..c6c95af --- /dev/null +++ b/tests/KubernetesClient.Tests/SerializationTests.cs @@ -0,0 +1,79 @@ +using k8s.Tests.Mock; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace k8s.Tests +{ + public class SerializationTests + { + private readonly ITestOutputHelper testOutput; + + private enum Animals + { + Dog, + Cat, + Mouse, + } + + public SerializationTests(ITestOutputHelper testOutput) + { + this.testOutput = testOutput; + } + + [Fact] + public async Task SerializeEnumUsingPascalCase() + { + using var server = new MockKubeApiServer(testOutput); + + var config = new KubernetesClientConfiguration { Host = server.Uri.ToString() }; + config.AddJsonOptions(options => + { + // Insert the converter at the front of the list so it overrides any others. + options.Converters.Insert(index: 0, new JsonStringEnumConverter()); + }); + var client = new Kubernetes(config); + + var customObject = Animals.Dog; + + var result = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(customObject, "TestGroup", "TestVersion", "TestNamespace", "TestPlural").ConfigureAwait(false); + var content = await result.Request.Content.ReadAsStringAsync().ConfigureAwait(false); + + // Assert that the client serializes using the default options. + Assert.Equal(@"""Dog""", content); + + // Assert that the underlying KubernetesJson serializes using the default options. + string animal = KubernetesJson.Serialize(Animals.Cat); + Assert.Equal(@"""Cat""", animal); + } + + [Fact] + public async Task SerializeEnumUsingCamelCase() + { + using var server = new MockKubeApiServer(testOutput); + + var config = new KubernetesClientConfiguration { Host = server.Uri.ToString() }; + config.AddJsonOptions(options => + { + // Insert the converter at the front of the list so it overrides + // the default JsonStringEnumConverter without namingPolicy. + options.Converters.Insert(index: 0, new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + }); + var client = new Kubernetes(config); + + var customObject = Animals.Dog; + + var result = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(customObject, "TestGroup", "TestVersion", "TestNamespace", "TestPlural").ConfigureAwait(false); + var content = await result.Request.Content.ReadAsStringAsync().ConfigureAwait(false); + + // Assert that the client serializes using the specified options. + Assert.Equal(@"""dog""", content); + + // Assert that the underlying KubernetesJson serializes using the default options. + string animal = KubernetesJson.Serialize(Animals.Cat); + Assert.Equal(@"""Cat""", animal); + } + } +} diff --git a/tests/KubernetesClient.Tests/WatchTests.cs b/tests/KubernetesClient.Tests/WatchTests.cs index bddf0c2..9eee491 100644 --- a/tests/KubernetesClient.Tests/WatchTests.cs +++ b/tests/KubernetesClient.Tests/WatchTests.cs @@ -445,8 +445,8 @@ namespace k8s.Tests var handler1 = new DummyHandler(); var handler2 = new DummyHandler(); - var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }, handler1, - handler2); + var client = new Kubernetes( + new KubernetesClientConfiguration { Host = server.Uri.ToString() }, handler1, handler2); Assert.False(handler1.Called); Assert.False(handler2.Called); @@ -732,12 +732,13 @@ namespace k8s.Tests return false; }); - var h = new CheckHeaderDelegatingHandler(); - var client = new Kubernetes(new KubernetesClientConfiguration { Host = server.Uri.ToString() }, h); + var handler = new CheckHeaderDelegatingHandler(); + var client = new Kubernetes( + new KubernetesClientConfiguration { Host = server.Uri.ToString() }, handler); - Assert.Null(h.Version); + Assert.Null(handler.Version); await client.CoreV1.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); - Assert.Equal(HttpVersion.Version20, h.Version); + Assert.Equal(HttpVersion.Version20, handler.Version); } } }