diff --git a/.editorconfig b/.editorconfig index 94bc4cb..935e327 100644 --- a/.editorconfig +++ b/.editorconfig @@ -116,6 +116,8 @@ csharp_space_between_method_call_parameter_list_parentheses = false:error csharp_preserve_single_line_statements = false:error csharp_preserve_single_line_blocks = true:error +csharp_style_namespace_declarations = file_scoped + # Resharper resharper_csharp_braces_for_lock=required_for_complex resharper_csharp_braces_for_using=required_for_complex diff --git a/README.md b/README.md index 51ac03d..989137d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ var response = transport.Get("/"); var headResponse = transport.Head("/"); ``` -`Get` and `Head` are extension methods to the only method `ITransport` dictates namely `Request()` and its async variant. +`Get` and `Head` are extension methods to the only method `HttpTransport` dictates namely `Request()` and its async variant. Wrapping clients most likely will list out all `components` explicitly and use `Transport` @@ -52,9 +52,9 @@ This allows implementers to extend `TransportConfiguration` with product/service ### Components -`ITransport` itself only defines `Request()` and `RequestAsync()` and all wrapping clients accept an `ITransport`. +`HttpTransport` itself only defines `Request()` and `RequestAsync()` and all wrapping clients accept an `HttpTransport`. -The `ITransport` implementation that this library ships models a request pipeline that can deal with a large variety of topologies +The `HttpTransport` implementation that this library ships models a request pipeline that can deal with a large variety of topologies ![request-pipeline.png](request-pipeline.png) @@ -96,7 +96,7 @@ the `IConnectionPool` to the transport configuration ONLY if a connection pool indicates it supports receiving new nodes will the transport sniff. * `IConnection` Abstraction for the actual IO the transport needs to perform. -* `ITransportSerializer` +* `HttpTransportSerializer` Allows you to inject your own serializer, the default uses `System.Text.Json` * `IProductRegistration` Product specific implementations and metadata provider @@ -106,24 +106,24 @@ Product specific implementations and metadata provider * `ITransportConfigurationValues` A transport configuration instance, explictly designed for clients to introduce subclasses of -* `IRequestPipelineFactory` -A factory creating `IRequestPipeline` instances -* `IDateTimeProvider` +* `RequestPipelineFactory` +A factory creating `RequestPipeline` instances +* `DateTimeProvider` Abstraction around the static `DateTime.Now` so we can test algorithms without waiting on the clock on the wall. -* `IMemoryStreamFactory` +* `MemoryStreamFactory` A factory creating `MemoryStream` instances. ### Observability -The default `ITransport` implementation ships with various `DiagnosticSources` to make the whole +The default `HttpTransport` implementation ships with various `DiagnosticSources` to make the whole flow through the request pipeline auditable and debuggable. -Every response returned by `Transport` has to implement `ITransportResponse` which has one property `ApiCall` of -type `IApiCallDetails` which in turns holds all information relevant to the request and response. +Every response returned by `Transport` has to implement `TransportResponse` which has one property `ApiCall` of +type `ApiCallDetails` which in turns holds all information relevant to the request and response. -`NOTE:` it also exposes `response.ApiCall.DebugInformation` always holds a human readable string to indicate +`NOTE:` it also exposes `response.ApiCallDetails.DebugInformation` always holds a human readable string to indicate what happened. Further more `DiagnosticSources` exists for various purposes e.g (de)serialization times, time to first byte & various counters diff --git a/benchmarks/Elastic.Transport.Benchmarks/TransportBenchmarks.cs b/benchmarks/Elastic.Transport.Benchmarks/TransportBenchmarks.cs index 6a4f3f7..ff77561 100644 --- a/benchmarks/Elastic.Transport.Benchmarks/TransportBenchmarks.cs +++ b/benchmarks/Elastic.Transport.Benchmarks/TransportBenchmarks.cs @@ -10,7 +10,7 @@ namespace Elastic.Transport.Benchmarks { public class TransportBenchmarks { - private Transport _transport; + private DefaultHttpTransport _transport; [GlobalSetup] public void Setup() @@ -19,7 +19,7 @@ public void Setup() var pool = new SingleNodePool(new Uri("http://localhost:9200")); var settings = new TransportConfiguration(pool, connection); - _transport = new Transport(settings); + _transport = new DefaultHttpTransport(settings); } [Benchmark] @@ -28,9 +28,11 @@ public void Setup() [Benchmark] public async Task TransportSuccessfulAsyncRequestBenchmark() => await _transport.GetAsync("/"); - private class EmptyResponse : ITransportResponse + private class EmptyResponse : TransportResponse { - public IApiCallDetails ApiCall { get; set; } + public EmptyResponse() : base() { } + + public ApiCallDetails ApiCall { get; set; } } } } diff --git a/benchmarks/Elastic.Transport.Profiling/Program.cs b/benchmarks/Elastic.Transport.Profiling/Program.cs index e620851..9543339 100644 --- a/benchmarks/Elastic.Transport.Profiling/Program.cs +++ b/benchmarks/Elastic.Transport.Profiling/Program.cs @@ -17,7 +17,7 @@ private static async Task Main() MemoryProfiler.GetSnapshot("start"); var config = new TransportConfiguration(new Uri("http://localhost:9200"), new ElasticsearchProductRegistration()); - var transport = new Transport(config); + var transport = new DefaultHttpTransport(config); _ = await transport.GetAsync("/"); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 089c230..7e33d2f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -18,7 +18,7 @@ true true - + 002400000480000094000000060200000024000052534131000400000100010015b0fa59d868c7f3ea2ae67567b19e102465745f01b430a38a42b92fd41a0f5869bec1f2b33b589d78662af432fe6b789ef72d4738f7b1a86264d7aeb5185ed8995b2bb104e7c5c58845f1a618be829e410fa34a6bd7d714ece191ed68a66333a83ae7456ee32e9aeb54bc1d7410ae8c344367257e9001abb5e96ce1f1d97696 diff --git a/src/Elastic.Transport.VirtualizedCluster/Audit/Auditor.cs b/src/Elastic.Transport.VirtualizedCluster/Audit/Auditor.cs index b5e39a2..5a61bfd 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Audit/Auditor.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Audit/Auditor.cs @@ -10,294 +10,292 @@ using Elastic.Transport.Extensions; using Elastic.Transport.VirtualizedCluster.Extensions; -namespace Elastic.Transport.VirtualizedCluster.Audit -{ - public class Auditor - { - private Components.VirtualizedCluster _cluster; - private Components.VirtualizedCluster _clusterAsync; +namespace Elastic.Transport.VirtualizedCluster.Audit; - public Auditor(Func setup) => Cluster = setup; +public sealed class Auditor +{ + private Components.VirtualizedCluster _cluster; + private Components.VirtualizedCluster _clusterAsync; - private Auditor(Components.VirtualizedCluster cluster, Components.VirtualizedCluster clusterAsync) - { - _cluster = cluster; - _clusterAsync = clusterAsync; - StartedUp = true; - } + public Auditor(Func setup) => Cluster = setup; - public Action AssertPoolAfterCall { get; set; } - public Action AssertPoolAfterStartup { get; set; } + private Auditor(Components.VirtualizedCluster cluster, Components.VirtualizedCluster clusterAsync) + { + _cluster = cluster; + _clusterAsync = clusterAsync; + StartedUp = true; + } - public Action AssertPoolBeforeCall { get; set; } - public Action AssertPoolBeforeStartup { get; set; } + public Action AssertPoolAfterCall { get; set; } + public Action AssertPoolAfterStartup { get; set; } - public IEnumerable AsyncAuditTrail { get; set; } - public IEnumerable AuditTrail { get; set; } - public Func Cluster { get; set; } + public Action AssertPoolBeforeCall { get; set; } + public Action AssertPoolBeforeStartup { get; set; } - public ITransportResponse Response { get; internal set; } - public ITransportResponse ResponseAsync { get; internal set; } + public IEnumerable AsyncAuditTrail { get; set; } + public IEnumerable AuditTrail { get; set; } + public Func Cluster { get; set; } - private bool StartedUp { get; } + public TransportResponse Response { get; internal set; } + public TransportResponse ResponseAsync { get; internal set; } + private bool StartedUp { get; } - public void ChangeTime(Func selector) - { - _cluster ??= Cluster(); - _clusterAsync ??= Cluster(); + public void ChangeTime(Func selector) + { + _cluster ??= Cluster(); + _clusterAsync ??= Cluster(); - _cluster.ChangeTime(selector); - _clusterAsync.ChangeTime(selector); - } + _cluster.ChangeTime(selector); + _clusterAsync.ChangeTime(selector); + } - public async Task TraceStartup(ClientCall callTrace = null) - { - //synchronous code path - _cluster = _cluster ?? Cluster(); - if (!StartedUp) AssertPoolBeforeStartup?.Invoke(_cluster.ConnectionPool); - AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); - Response = _cluster.ClientCall(callTrace?.RequestOverrides); - AuditTrail = Response.ApiCall.AuditTrail; - if (!StartedUp) AssertPoolAfterStartup?.Invoke(_cluster.ConnectionPool); - AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); - - //async code path - _clusterAsync = _clusterAsync ?? Cluster(); - if (!StartedUp) AssertPoolBeforeStartup?.Invoke(_clusterAsync.ConnectionPool); - AssertPoolBeforeCall?.Invoke(_clusterAsync.ConnectionPool); - ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); - AsyncAuditTrail = ResponseAsync.ApiCall.AuditTrail; - if (!StartedUp) AssertPoolAfterStartup?.Invoke(_clusterAsync.ConnectionPool); - AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); - return new Auditor(_cluster, _clusterAsync); - } + public async Task TraceStartup(ClientCall callTrace = null) + { + //synchronous code path + _cluster ??= Cluster(); + if (!StartedUp) AssertPoolBeforeStartup?.Invoke(_cluster.ConnectionPool); + AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); + Response = _cluster.ClientCall(callTrace?.RequestOverrides); + AuditTrail = Response.ApiCallDetails.AuditTrail; + if (!StartedUp) AssertPoolAfterStartup?.Invoke(_cluster.ConnectionPool); + AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); + + //async code path + _clusterAsync ??= Cluster(); + if (!StartedUp) AssertPoolBeforeStartup?.Invoke(_clusterAsync.ConnectionPool); + AssertPoolBeforeCall?.Invoke(_clusterAsync.ConnectionPool); + ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); + AsyncAuditTrail = ResponseAsync.ApiCallDetails.AuditTrail; + if (!StartedUp) AssertPoolAfterStartup?.Invoke(_clusterAsync.ConnectionPool); + AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); + return new Auditor(_cluster, _clusterAsync); + } - public async Task TraceCall(ClientCall callTrace, int nthCall = 0) - { - await TraceStartup(callTrace).ConfigureAwait(false); - return AssertAuditTrails(callTrace, nthCall); - } + public async Task TraceCall(ClientCall callTrace, int nthCall = 0) + { + await TraceStartup(callTrace).ConfigureAwait(false); + return AssertAuditTrails(callTrace, nthCall); + } #pragma warning disable 1998 // Async method lacks 'await' operators and will run synchronously - private async Task TraceException(ClientCall callTrace, Action assert) + private async Task TraceException(ClientCall callTrace, Action assert) #pragma warning restore 1998 // Async method lacks 'await' operators and will run synchronously - where TException : TransportException - { - _cluster = _cluster ?? Cluster(); - _cluster.ClientThrows(true); - AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); + where TException : TransportException + { + _cluster ??= Cluster(); + _cluster.ClientThrows(true); + AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); - Action call = () => Response = _cluster.ClientCall(callTrace?.RequestOverrides); - var exception = TryCall(call, assert); - assert(exception); + Action call = () => Response = _cluster.ClientCall(callTrace?.RequestOverrides); + var exception = TryCall(call, assert); + assert(exception); - AuditTrail = exception.AuditTrail; - AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); + AuditTrail = exception.AuditTrail; + AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); - _clusterAsync = _clusterAsync ?? Cluster(); - _clusterAsync.ClientThrows(true); - Func callAsync = async () => ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); - exception = await TryCallAsync(callAsync, assert).ConfigureAwait(false); - assert(exception); + _clusterAsync ??= Cluster(); + _clusterAsync.ClientThrows(true); + Func callAsync = async () => ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); + exception = await TryCallAsync(callAsync, assert).ConfigureAwait(false); + assert(exception); - AsyncAuditTrail = exception.AuditTrail; - AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); - } + AsyncAuditTrail = exception.AuditTrail; + AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); + } - public async Task TraceElasticsearchException(ClientCall callTrace, Action assert) - { - await TraceException(callTrace, assert).ConfigureAwait(false); - var audit = new Auditor(_cluster, _clusterAsync); - return await audit.TraceElasticsearchExceptionOnResponse(callTrace, assert).ConfigureAwait(false); - } + public async Task TraceElasticsearchException(ClientCall callTrace, Action assert) + { + await TraceException(callTrace, assert).ConfigureAwait(false); + var audit = new Auditor(_cluster, _clusterAsync); + return await audit.TraceElasticsearchExceptionOnResponse(callTrace, assert).ConfigureAwait(false); + } - public async Task TraceUnexpectedTransportException(ClientCall callTrace, Action assert) - { - await TraceException(callTrace, assert).ConfigureAwait(false); - return new Auditor(_cluster, _clusterAsync); - } + public async Task TraceUnexpectedTransportException(ClientCall callTrace, Action assert) + { + await TraceException(callTrace, assert).ConfigureAwait(false); + return new Auditor(_cluster, _clusterAsync); + } #pragma warning disable 1998 - public async Task TraceElasticsearchExceptionOnResponse(ClientCall callTrace, Action assert) + public async Task TraceElasticsearchExceptionOnResponse(ClientCall callTrace, Action assert) #pragma warning restore 1998 - { - _cluster = _cluster ?? Cluster(); - _cluster.ClientThrows(false); - AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); + { + _cluster ??= Cluster(); + _cluster.ClientThrows(false); + AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); - Action call = () => { Response = _cluster.ClientCall(callTrace?.RequestOverrides); }; - call(); + Action call = () => { Response = _cluster.ClientCall(callTrace?.RequestOverrides); }; + call(); - if (Response.ApiCall.Success) throw new Exception("Expected call to not be valid"); + if (Response.ApiCallDetails.Success) throw new Exception("Expected call to not be valid"); - if (Response.ApiCall.OriginalException is not TransportException exception) - throw new Exception("OriginalException on response is not expected TransportException"); + if (Response.ApiCallDetails.OriginalException is not TransportException exception) + throw new Exception("OriginalException on response is not expected TransportException"); - assert(exception); + assert(exception); - AuditTrail = exception.AuditTrail; - AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); + AuditTrail = exception.AuditTrail; + AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); - _clusterAsync = _clusterAsync ?? Cluster(); - _clusterAsync.ClientThrows(false); - Func callAsync = async () => { ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); }; - await callAsync().ConfigureAwait(false); - if (Response.ApiCall.Success) throw new Exception("Expected call to not be valid"); - exception = ResponseAsync.ApiCall.OriginalException as TransportException; - if (exception == null) throw new Exception("OriginalException on response is not expected TransportException"); - assert(exception); + _clusterAsync ??= Cluster(); + _clusterAsync.ClientThrows(false); + Func callAsync = async () => { ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); }; + await callAsync().ConfigureAwait(false); + if (Response.ApiCallDetails.Success) throw new Exception("Expected call to not be valid"); + exception = ResponseAsync.ApiCallDetails.OriginalException as TransportException; + if (exception == null) throw new Exception("OriginalException on response is not expected TransportException"); + assert(exception); - AsyncAuditTrail = exception.AuditTrail; - AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); - var audit = new Auditor(_cluster, _clusterAsync); + AsyncAuditTrail = exception.AuditTrail; + AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); + var audit = new Auditor(_cluster, _clusterAsync); - return audit; - } + return audit; + } #pragma warning disable 1998 - public async Task TraceUnexpectedException(ClientCall callTrace, Action assert) + public async Task TraceUnexpectedException(ClientCall callTrace, Action assert) #pragma warning restore 1998 - { - _cluster = _cluster ?? Cluster(); - AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); + { + _cluster ??= Cluster(); + AssertPoolBeforeCall?.Invoke(_cluster.ConnectionPool); - Action call = () => Response = _cluster.ClientCall(callTrace?.RequestOverrides); - var exception = TryCall(call, assert); - assert(exception); + Action call = () => Response = _cluster.ClientCall(callTrace?.RequestOverrides); + var exception = TryCall(call, assert); + assert(exception); - AuditTrail = exception.AuditTrail; - AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); + AuditTrail = exception.AuditTrail; + AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); - _clusterAsync = _clusterAsync ?? Cluster(); - Func callAsync = async () => ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); - exception = await TryCallAsync(callAsync, assert).ConfigureAwait(false); - assert(exception); + _clusterAsync ??= Cluster(); + Func callAsync = async () => ResponseAsync = await _clusterAsync.ClientCallAsync(callTrace?.RequestOverrides).ConfigureAwait(false); + exception = await TryCallAsync(callAsync, assert).ConfigureAwait(false); + assert(exception); - AsyncAuditTrail = exception.AuditTrail; - AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); - return new Auditor(_cluster, _clusterAsync); - } + AsyncAuditTrail = exception.AuditTrail; + AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); + return new Auditor(_cluster, _clusterAsync); + } - private Auditor AssertAuditTrails(ClientCall callTrace, int nthCall) - { - var nl = Environment.NewLine; + private Auditor AssertAuditTrails(ClientCall callTrace, int nthCall) + { + var nl = Environment.NewLine; - if (AuditTrail is ICollection at && AsyncAuditTrail is ICollection ata && at.Count != ata.Count) - throw new Exception($"{nthCall} has a mismatch between sync and async. {nl}async:{AuditTrail}{nl}sync:{AsyncAuditTrail}"); - else if (AuditTrail.Count() != AsyncAuditTrail.Count()) - throw new Exception($"{nthCall} has a mismatch between sync and async. {nl}async:{AuditTrail}{nl}sync:{AsyncAuditTrail}"); + if (AuditTrail is ICollection at && AsyncAuditTrail is ICollection ata && at.Count != ata.Count) + throw new Exception($"{nthCall} has a mismatch between sync and async. {nl}async:{AuditTrail}{nl}sync:{AsyncAuditTrail}"); + else if (AuditTrail.Count() != AsyncAuditTrail.Count()) + throw new Exception($"{nthCall} has a mismatch between sync and async. {nl}async:{AuditTrail}{nl}sync:{AsyncAuditTrail}"); - AssertTrailOnResponse(callTrace, AuditTrail, true, nthCall); - AssertTrailOnResponse(callTrace, AuditTrail, false, nthCall); + AssertTrailOnResponse(callTrace, AuditTrail, true, nthCall); + AssertTrailOnResponse(callTrace, AuditTrail, false, nthCall); - callTrace.AssertResponse?.Invoke(Response); - callTrace.AssertResponse?.Invoke(ResponseAsync); + callTrace.AssertResponse?.Invoke(Response); + callTrace.AssertResponse?.Invoke(ResponseAsync); - callTrace.AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); - callTrace.AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); - return new Auditor(_cluster, _clusterAsync); - } + callTrace.AssertPoolAfterCall?.Invoke(_cluster.ConnectionPool); + callTrace.AssertPoolAfterCall?.Invoke(_clusterAsync.ConnectionPool); + return new Auditor(_cluster, _clusterAsync); + } - public void VisualizeCalls(int numberOfCalls) + public void VisualizeCalls(int numberOfCalls) + { + var cluster = _cluster ?? Cluster(); + var messages = new List(numberOfCalls * 2); + for (var i = 0; i < numberOfCalls; i++) { - var cluster = _cluster ?? Cluster(); - var messages = new List(numberOfCalls * 2); - for (var i = 0; i < numberOfCalls; i++) - { - var call = cluster.ClientCall(); - var d = call.ApiCall; - var actualAuditTrail = AuditTrailToString(d.AuditTrail); - messages.Add($"{d.HttpMethod.GetStringValue()} ({d.Uri.Port}) (status: {d.HttpStatusCode})"); - messages.Add(actualAuditTrail); - } - throw new Exception(string.Join(Environment.NewLine, messages)); + var call = cluster.ClientCall(); + var d = call.ApiCallDetails; + var actualAuditTrail = AuditTrailToString(d.AuditTrail); + messages.Add($"{d.HttpMethod.GetStringValue()} ({d.Uri.Port}) (status: {d.HttpStatusCode})"); + messages.Add(actualAuditTrail); } + throw new Exception(string.Join(Environment.NewLine, messages)); + } + + private static string AuditTrailToString(IEnumerable auditTrail) + { + var actualAuditTrail = auditTrail.Aggregate(new StringBuilder(), + (sb, a) => sb.AppendLine($"-> {a}"), + sb => sb.ToString()); + return actualAuditTrail; + } + + public async Task TraceCalls(params ClientCall[] audits) + { + var auditor = this; + foreach (var a in audits.Select((a, i) => new { a, i })) auditor = await auditor.TraceCall(a.a, a.i).ConfigureAwait(false); + return auditor; + } + + private static void AssertTrailOnResponse(ClientCall callTrace, IEnumerable auditTrail, bool sync, int nthCall) + { + var typeOfTrail = (sync ? "synchronous" : "asynchronous") + " audit trail"; + var nthClientCall = (nthCall + 1).ToOrdinal(); + + var actualAuditTrail = auditTrail.Aggregate(new StringBuilder(Environment.NewLine), + (sb, a) => sb.AppendLine($"-> {a}"), + sb => sb.ToString()); + + var traceEvents = callTrace.Select(c => c.Event).ToList(); + var auditEvents = auditTrail.Select(a => a.Event).ToList(); + if (!traceEvents.SequenceEqual(auditEvents)) + throw new Exception($"the {nthClientCall} client call's {typeOfTrail} should assert ALL audit trail items{actualAuditTrail}"); - private static string AuditTrailToString(IEnumerable auditTrail) + foreach (var t in auditTrail.Select((a, i) => new { a, i })) { - var actualAuditTrail = auditTrail.Aggregate(new StringBuilder(), - (sb, a) => sb.AppendLine($"-> {a}"), - sb => sb.ToString()); - return actualAuditTrail; + var i = t.i; + var audit = t.a; + var nthAuditTrailItem = (i + 1).ToOrdinal(); + var because = $"thats the {{0}} specified on the {nthAuditTrailItem} item in the {nthClientCall} client call's {typeOfTrail}"; + var c = callTrace[i]; + if (audit.Event != c.Event) + throw new Exception(string.Format(because, "event")); + if (c.Port.HasValue && audit.Node.Uri.Port != c.Port.Value) + throw new Exception(string.Format(because, "port")); + + c.SimpleAssert?.Invoke(audit); + c.AssertWithBecause?.Invoke(string.Format(because, "custom assertion"), audit); } - public async Task TraceCalls(params ClientCall[] audits) + if (auditTrail is ICollection at && callTrace.Count != at.Count) + throw new Exception($"callTrace has {callTrace.Count} items. Actual auditTrail {actualAuditTrail}"); + else if (callTrace.Count != auditTrail.Count()) + throw new Exception($"callTrace has {callTrace.Count} items. Actual auditTrail {actualAuditTrail}"); + } + + private static TException TryCall(Action call, Action assert) where TException : TransportException + { + TException exception = null; + try { - var auditor = this; - foreach (var a in audits.Select((a, i) => new { a, i })) auditor = await auditor.TraceCall(a.a, a.i).ConfigureAwait(false); - return auditor; + call(); } - - private static void AssertTrailOnResponse(ClientCall callTrace, IEnumerable auditTrail, bool sync, int nthCall) + catch (TException ex) { - var typeOfTrail = (sync ? "synchronous" : "asynchronous") + " audit trail"; - var nthClientCall = (nthCall + 1).ToOrdinal(); - - var actualAuditTrail = auditTrail.Aggregate(new StringBuilder(Environment.NewLine), - (sb, a) => sb.AppendLine($"-> {a}"), - sb => sb.ToString()); - - var traceEvents =callTrace.Select(c => c.Event).ToList(); - var auditEvents = auditTrail.Select(a => a.Event).ToList(); - if (!traceEvents.SequenceEqual(auditEvents)) - throw new Exception($"the {nthClientCall} client call's {typeOfTrail} should assert ALL audit trail items{actualAuditTrail}"); - - foreach (var t in auditTrail.Select((a, i) => new { a, i })) - { - var i = t.i; - var audit = t.a; - var nthAuditTrailItem = (i + 1).ToOrdinal(); - var because = $"thats the {{0}} specified on the {nthAuditTrailItem} item in the {nthClientCall} client call's {typeOfTrail}"; - var c = callTrace[i]; - if (audit.Event != c.Event) - throw new Exception(string.Format(because, "event")); - if (c.Port.HasValue && audit.Node.Uri.Port != c.Port.Value) - throw new Exception(string.Format(because, "port")); - - c.SimpleAssert?.Invoke(audit); - c.AssertWithBecause?.Invoke(string.Format(because, "custom assertion"), audit); - } - - if (auditTrail is ICollection at && callTrace.Count != at.Count) - throw new Exception($"callTrace has {callTrace.Count} items. Actual auditTrail {actualAuditTrail}"); - else if (callTrace.Count != auditTrail.Count()) - throw new Exception($"callTrace has {callTrace.Count} items. Actual auditTrail {actualAuditTrail}"); + exception = ex; + assert(ex); } + if (exception is null) throw new Exception("No exception happened while one was expected"); - private static TException TryCall(Action call, Action assert) where TException : TransportException + return exception; + } + private static async Task TryCallAsync(Func call, Action assert) where TException : TransportException + { + TException exception = null; + try { - TException exception = null; - try - { - call(); - } - catch (TException ex) - { - exception = ex; - assert(ex); - } - if (exception is null) throw new Exception("No exception happened while one was expected"); - - return exception; + await call().ConfigureAwait(false); } - private static async Task TryCallAsync(Func call, Action assert) where TException : TransportException + catch (TException ex) { - TException exception = null; - try - { - await call().ConfigureAwait(false); - } - catch (TException ex) - { - exception = ex; - assert(ex); - } - if (exception is null) throw new Exception("No exception happened while one was expected"); - - return exception; + exception = ex; + assert(ex); } + if (exception is null) throw new Exception("No exception happened while one was expected"); + return exception; } + } diff --git a/src/Elastic.Transport.VirtualizedCluster/Audit/Audits.cs b/src/Elastic.Transport.VirtualizedCluster/Audit/Audits.cs deleted file mode 100644 index 10ed674..0000000 --- a/src/Elastic.Transport.VirtualizedCluster/Audit/Audits.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using Elastic.Transport.Diagnostics.Auditing; - -namespace Elastic.Transport.VirtualizedCluster.Audit -{ - public class CallTraceState - { - public CallTraceState(AuditEvent e) => Event = e; - - public Action AssertWithBecause { get; set; } - - public AuditEvent Event { get; private set; } - - public int? Port { get; set; } - - public Action SimpleAssert { get; set; } - } - - public class ClientCall : List - { - public ClientCall() { } - - public ClientCall(Func requestOverrides) => RequestOverrides = requestOverrides; - - public Action AssertPoolAfterCall { get; private set; } - public Action AssertResponse { get; private set; } - public Func RequestOverrides { get; } - - public void Add(AuditEvent key, Action value) => Add(new CallTraceState(key) { SimpleAssert = value }); - - public void Add(AuditEvent key, int port) => Add(new CallTraceState(key) { Port = port }); - - public void Add(AuditEvent key) => Add(new CallTraceState(key)); - - public void Add(Action pool) => AssertPoolAfterCall = pool; - - public void Add(AuditEvent key, int port, Action assertResponse) - { - Add(new CallTraceState(key) { Port = port }); - AssertResponse = assertResponse; - } - } -} diff --git a/src/Elastic.Transport.VirtualizedCluster/Audit/CallTraceState.cs b/src/Elastic.Transport.VirtualizedCluster/Audit/CallTraceState.cs new file mode 100644 index 0000000..52af62f --- /dev/null +++ b/src/Elastic.Transport.VirtualizedCluster/Audit/CallTraceState.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using Elastic.Transport.Diagnostics.Auditing; + +namespace Elastic.Transport.VirtualizedCluster.Audit; + +public sealed class CallTraceState +{ + public CallTraceState(AuditEvent e) => Event = e; + + public Action AssertWithBecause { get; set; } + + public AuditEvent Event { get; private set; } + + public int? Port { get; set; } + + public Action SimpleAssert { get; set; } +} diff --git a/src/Elastic.Transport.VirtualizedCluster/Audit/ClientCall.cs b/src/Elastic.Transport.VirtualizedCluster/Audit/ClientCall.cs new file mode 100644 index 0000000..b10ab8b --- /dev/null +++ b/src/Elastic.Transport.VirtualizedCluster/Audit/ClientCall.cs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using Elastic.Transport.Diagnostics.Auditing; + +namespace Elastic.Transport.VirtualizedCluster.Audit; + +public sealed class ClientCall : List +{ + public ClientCall() { } + + public ClientCall(Func requestOverrides) => RequestOverrides = requestOverrides; + + public Action AssertPoolAfterCall { get; private set; } + public Action AssertResponse { get; private set; } + public Func RequestOverrides { get; } + + public void Add(AuditEvent key, Action value) => Add(new CallTraceState(key) { SimpleAssert = value }); + + public void Add(AuditEvent key, int port) => Add(new CallTraceState(key) { Port = port }); + + public void Add(AuditEvent key) => Add(new CallTraceState(key)); + + public void Add(Action pool) => AssertPoolAfterCall = pool; + + public void Add(AuditEvent key, int port, Action assertResponse) + { + Add(new CallTraceState(key) { Port = port }); + AssertResponse = assertResponse; + } +} diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs b/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs index 389eb5f..7bee834 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs @@ -2,35 +2,30 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport.VirtualizedCluster.Components +namespace Elastic.Transport.VirtualizedCluster.Components; + +/// +/// An implementation that exposes all the components so that can reference them directly. +/// +public sealed class ExposingPipelineFactory : RequestPipelineFactory where TConfiguration : class, ITransportConfiguration { - /// - /// An implementation that exposes all the components so that can reference them directly. - /// - public class ExposingPipelineFactory : IRequestPipelineFactory where TConfiguration : class, ITransportConfiguration + public ExposingPipelineFactory(TConfiguration connectionSettings, DateTimeProvider dateTimeProvider) { - public ExposingPipelineFactory(TConfiguration connectionSettings, IDateTimeProvider dateTimeProvider) - { - DateTimeProvider = dateTimeProvider; - MemoryStreamFactory = TransportConfiguration.DefaultMemoryStreamFactory; - - Settings = connectionSettings; - Pipeline = Create(Settings, DateTimeProvider, MemoryStreamFactory, new RequestParameters()); - Transport = new Transport(Settings, this, DateTimeProvider, MemoryStreamFactory); - } - - // ReSharper disable once MemberCanBePrivate.Global - public IRequestPipeline Pipeline { get; } - - private IDateTimeProvider DateTimeProvider { get; } - private IMemoryStreamFactory MemoryStreamFactory { get; } - private TConfiguration Settings { get; } - public ITransport Transport { get; } + DateTimeProvider = dateTimeProvider; + MemoryStreamFactory = TransportConfiguration.DefaultMemoryStreamFactory; + Settings = connectionSettings; + Pipeline = Create(Settings, DateTimeProvider, MemoryStreamFactory, new DefaultRequestParameters()); + Transport = new DefaultHttpTransport(Settings, this, DateTimeProvider, MemoryStreamFactory); + } + // ReSharper disable once MemberCanBePrivate.Global + public RequestPipeline Pipeline { get; } + private DateTimeProvider DateTimeProvider { get; } + private MemoryStreamFactory MemoryStreamFactory { get; } + private TConfiguration Settings { get; } + public HttpTransport Transport { get; } - public IRequestPipeline Create(TConfiguration configurationValues, IDateTimeProvider dateTimeProvider, - IMemoryStreamFactory memoryStreamFactory, IRequestParameters requestParameters - ) => - new RequestPipeline(Settings, DateTimeProvider, MemoryStreamFactory, requestParameters ?? new RequestParameters()); - } + public override RequestPipeline Create(TConfiguration configurationValues, DateTimeProvider dateTimeProvider, + MemoryStreamFactory memoryStreamFactory, RequestParameters requestParameters) => + new DefaultRequestPipeline(Settings, DateTimeProvider, MemoryStreamFactory, requestParameters ?? new DefaultRequestParameters()); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs b/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs index 5887610..5da5ee5 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs @@ -6,46 +6,45 @@ using Elastic.Transport.VirtualizedCluster.Products; using Elastic.Transport.VirtualizedCluster.Providers; -namespace Elastic.Transport.VirtualizedCluster.Components +namespace Elastic.Transport.VirtualizedCluster.Components; + +/// +/// A continuation of 's builder methods that creates +/// an instance of for the cluster after which the components such as +/// and can no longer be updated. +/// +public sealed class SealedVirtualCluster { - /// - /// A continuation of 's builder methods that creates - /// an instance of for the cluster after which the components such as - /// and can no longer be updated. - /// - public class SealedVirtualCluster + private readonly TransportClient _connection; + private readonly NodePool _connectionPool; + private readonly TestableDateTimeProvider _dateTimeProvider; + private readonly MockProductRegistration _productRegistration; + + internal SealedVirtualCluster(VirtualCluster cluster, NodePool pool, TestableDateTimeProvider dateTimeProvider, MockProductRegistration productRegistration) { - private readonly ITransportClient _connection; - private readonly NodePool _connectionPool; - private readonly TestableDateTimeProvider _dateTimeProvider; - private readonly IMockProductRegistration _productRegistration; - - internal SealedVirtualCluster(VirtualCluster cluster, NodePool pool, TestableDateTimeProvider dateTimeProvider, IMockProductRegistration productRegistration) - { - _connectionPool = pool; - _connection = new VirtualClusterConnection(cluster, dateTimeProvider); - _dateTimeProvider = dateTimeProvider; - _productRegistration = productRegistration; - } - - private TransportConfiguration CreateSettings() => - new TransportConfiguration(_connectionPool, _connection, serializer: null, _productRegistration.ProductRegistration); - - /// Create the cluster using all defaults on - public VirtualizedCluster AllDefaults() => - new VirtualizedCluster(_dateTimeProvider, CreateSettings()); - - /// Create the cluster using to provide configuration changes - /// Provide custom configuration options - public VirtualizedCluster Settings(Func selector) => - new VirtualizedCluster(_dateTimeProvider, selector(CreateSettings())); - - /// - /// Allows you to create an instance of ` using the DSL provided by - /// - /// Provide custom configuration options - public VirtualClusterConnection VirtualClusterConnection(Func selector = null) => - new VirtualizedCluster(_dateTimeProvider, selector == null ? CreateSettings() : selector(CreateSettings())) - .Connection; + _connectionPool = pool; + _connection = new VirtualClusterConnection(cluster, dateTimeProvider); + _dateTimeProvider = dateTimeProvider; + _productRegistration = productRegistration; } + + private TransportConfiguration CreateSettings() => + new(_connectionPool, _connection, serializer: null, _productRegistration.ProductRegistration); + + /// Create the cluster using all defaults on + public VirtualizedCluster AllDefaults() => + new(_dateTimeProvider, CreateSettings()); + + /// Create the cluster using to provide configuration changes + /// Provide custom configuration options + public VirtualizedCluster Settings(Func selector) => + new(_dateTimeProvider, selector(CreateSettings())); + + /// + /// Allows you to create an instance of ` using the DSL provided by + /// + /// Provide custom configuration options + public VirtualClusterConnection VirtualClusterConnection(Func selector = null) => + new VirtualizedCluster(_dateTimeProvider, selector == null ? CreateSettings() : selector(CreateSettings())) + .Connection; } diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualCluster.cs b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualCluster.cs index 359a635..9f0ac77 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualCluster.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualCluster.cs @@ -9,97 +9,96 @@ using Elastic.Transport.VirtualizedCluster.Providers; using Elastic.Transport.VirtualizedCluster.Rules; -namespace Elastic.Transport.VirtualizedCluster.Components +namespace Elastic.Transport.VirtualizedCluster.Components; + +public class VirtualCluster { - public class VirtualCluster + protected VirtualCluster(IEnumerable nodes, MockProductRegistration productRegistration) + { + ProductRegistration = productRegistration; + InternalNodes = nodes.ToList(); + } + + public List ClientCallRules { get; } = new List(); + public TestableDateTimeProvider DateTimeProvider { get; } = new TestableDateTimeProvider(); + + protected List InternalNodes { get; } + public IReadOnlyList Nodes => InternalNodes; + public List PingingRules { get; } = new List(); + + public List SniffingRules { get; } = new List(); + internal string PublishAddressOverride { get; private set; } + + internal bool SniffShouldReturnFqnd { get; private set; } + internal string ElasticsearchVersion { get; private set; } = "7.0.0"; + + public MockProductRegistration ProductRegistration { get; } + + public VirtualCluster SniffShouldReturnFqdn() + { + SniffShouldReturnFqnd = true; + return this; + } + + public VirtualCluster SniffElasticsearchVersionNumber(string version) + { + ElasticsearchVersion = version; + return this; + } + + public VirtualCluster PublishAddress(string publishHost) + { + PublishAddressOverride = publishHost; + return this; + } + + public VirtualCluster Ping(Func selector) + { + PingingRules.Add(selector(new PingRule())); + return this; + } + + public VirtualCluster Sniff(Func selector) + { + SniffingRules.Add(selector(new SniffRule())); + return this; + } + + public VirtualCluster ClientCalls(Func selector) + { + ClientCallRules.Add(selector(new ClientCallRule())); + return this; + } + + public SealedVirtualCluster SingleNodeConnection(Func, IEnumerable> seedNodesSelector = null) + { + var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; + return new SealedVirtualCluster(this, new SingleNodePool(nodes.First().Uri), DateTimeProvider, ProductRegistration); + } + + public SealedVirtualCluster StaticNodePool(Func, IEnumerable> seedNodesSelector = null) + { + var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; + return new SealedVirtualCluster(this, new StaticNodePool(nodes, false, DateTimeProvider), DateTimeProvider, ProductRegistration); + } + + public SealedVirtualCluster SniffingNodePool(Func, IEnumerable> seedNodesSelector = null) + { + var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; + return new SealedVirtualCluster(this, new SniffingNodePool(nodes, false, DateTimeProvider), DateTimeProvider, ProductRegistration); + } + + public SealedVirtualCluster StickyNodePool(Func, IEnumerable> seedNodesSelector = null) + { + var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; + return new SealedVirtualCluster(this, new StickyNodePool(nodes, DateTimeProvider), DateTimeProvider, ProductRegistration); + } + + public SealedVirtualCluster StickySniffingNodePool(Func sorter = null, + Func, IEnumerable> seedNodesSelector = null + ) { - protected VirtualCluster(IEnumerable nodes, IMockProductRegistration productRegistration) - { - ProductRegistration = productRegistration; - InternalNodes = nodes.ToList(); - } - - public List ClientCallRules { get; } = new List(); - public TestableDateTimeProvider DateTimeProvider { get; } = new TestableDateTimeProvider(); - - protected List InternalNodes { get; } - public IReadOnlyList Nodes => InternalNodes; - public List PingingRules { get; } = new List(); - - public List SniffingRules { get; } = new List(); - internal string PublishAddressOverride { get; private set; } - - internal bool SniffShouldReturnFqnd { get; private set; } - internal string ElasticsearchVersion { get; private set; } = "7.0.0"; - - public IMockProductRegistration ProductRegistration { get; } - - public VirtualCluster SniffShouldReturnFqdn() - { - SniffShouldReturnFqnd = true; - return this; - } - - public VirtualCluster SniffElasticsearchVersionNumber(string version) - { - ElasticsearchVersion = version; - return this; - } - - public VirtualCluster PublishAddress(string publishHost) - { - PublishAddressOverride = publishHost; - return this; - } - - public VirtualCluster Ping(Func selector) - { - PingingRules.Add(selector(new PingRule())); - return this; - } - - public VirtualCluster Sniff(Func selector) - { - SniffingRules.Add(selector(new SniffRule())); - return this; - } - - public VirtualCluster ClientCalls(Func selector) - { - ClientCallRules.Add(selector(new ClientCallRule())); - return this; - } - - public SealedVirtualCluster SingleNodeConnection(Func, IEnumerable> seedNodesSelector = null) - { - var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; - return new SealedVirtualCluster(this, new SingleNodePool(nodes.First().Uri), DateTimeProvider, ProductRegistration); - } - - public SealedVirtualCluster StaticNodePool(Func, IEnumerable> seedNodesSelector = null) - { - var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; - return new SealedVirtualCluster(this, new StaticNodePool(nodes, false, DateTimeProvider), DateTimeProvider, ProductRegistration); - } - - public SealedVirtualCluster SniffingNodePool(Func, IEnumerable> seedNodesSelector = null) - { - var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; - return new SealedVirtualCluster(this, new SniffingNodePool(nodes, false, DateTimeProvider), DateTimeProvider, ProductRegistration); - } - - public SealedVirtualCluster StickyNodePool(Func, IEnumerable> seedNodesSelector = null) - { - var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; - return new SealedVirtualCluster(this, new StickyNodePool(nodes, DateTimeProvider), DateTimeProvider, ProductRegistration); - } - - public SealedVirtualCluster StickySniffingNodePool(Func sorter = null, - Func, IEnumerable> seedNodesSelector = null - ) - { - var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; - return new SealedVirtualCluster(this, new StickySniffingNodePool(nodes, sorter, DateTimeProvider), DateTimeProvider, ProductRegistration); - } + var nodes = seedNodesSelector?.Invoke(InternalNodes) ?? InternalNodes; + return new SealedVirtualCluster(this, new StickySniffingNodePool(nodes, sorter, DateTimeProvider), DateTimeProvider, ProductRegistration); } } diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualClusterConnection.cs b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualClusterConnection.cs index 2e4f664..26df108 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualClusterConnection.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualClusterConnection.cs @@ -18,297 +18,296 @@ using TheException = System.Net.WebException; #endif -namespace Elastic.Transport.VirtualizedCluster.Components +namespace Elastic.Transport.VirtualizedCluster.Components; + +/// +/// An in memory connection that uses a rule engine to return different responses for sniffs/pings and API calls. +///
+/// Either instantiate through the static  or  for the simplest use-cases
+/// 
+///
+/// Or use  to chain together a rule engine until
+///  becomes available
+/// 
+///
+public class VirtualClusterConnection : InMemoryConnection { - /// - /// An in memory connection that uses a rule engine to return different responses for sniffs/pings and API calls. - ///
-	/// Either instantiate through the static  or  for the simplest use-cases
-	/// 
- ///
-	/// Or use  to chain together a rule engine until
-	///  becomes available
-	/// 
- ///
- public class VirtualClusterConnection : InMemoryConnection - { - private static readonly object Lock = new object(); + private static readonly object Lock = new(); - private static byte[] _defaultResponseBytes; + private static byte[] _defaultResponseBytes; - private VirtualCluster _cluster; - private readonly TestableDateTimeProvider _dateTimeProvider; - private IMockProductRegistration _productRegistration; - private IDictionary _calls = new Dictionary(); + private VirtualCluster _cluster; + private readonly TestableDateTimeProvider _dateTimeProvider; + private MockProductRegistration _productRegistration; + private IDictionary _calls = new Dictionary(); - internal VirtualClusterConnection(VirtualCluster cluster, TestableDateTimeProvider dateTimeProvider) - { - UpdateCluster(cluster); - _dateTimeProvider = dateTimeProvider; - _productRegistration = cluster.ProductRegistration; - } + internal VirtualClusterConnection(VirtualCluster cluster, TestableDateTimeProvider dateTimeProvider) + { + UpdateCluster(cluster); + _dateTimeProvider = dateTimeProvider; + _productRegistration = cluster.ProductRegistration; + } + + /// + /// Create a instance that always returns a successful response. + /// + /// The bytes to be returned on every API call invocation + public static VirtualClusterConnection Success(byte[] response) => + Virtual.Elasticsearch + .Bootstrap(1) + .ClientCalls(r => r.SucceedAlways().ReturnByteResponse(response)) + .StaticNodePool() + .AllDefaults() + .Connection; - /// - /// Create a instance that always returns a successful response. - /// - /// The bytes to be returned on every API call invocation - public static VirtualClusterConnection Success(byte[] response) => - Virtual.Elasticsearch - .Bootstrap(1) - .ClientCalls(r => r.SucceedAlways().ReturnByteResponse(response)) - .StaticNodePool() - .AllDefaults() - .Connection; - - /// - /// Create a instance that always returns a failed response. - /// - public static VirtualClusterConnection Error() => - Virtual.Elasticsearch - .Bootstrap(1) - .ClientCalls(r => r.FailAlways(400)) - .StaticNodePool() - .AllDefaults() - .Connection; - - private static object DefaultResponse + /// + /// Create a instance that always returns a failed response. + /// + public static VirtualClusterConnection Error() => + Virtual.Elasticsearch + .Bootstrap(1) + .ClientCalls(r => r.FailAlways(400)) + .StaticNodePool() + .AllDefaults() + .Connection; + + private static object DefaultResponse + { + get { - get + var response = new { - var response = new + name = "Razor Fist", + cluster_name = "elasticsearch-test-cluster", + version = new { - name = "Razor Fist", - cluster_name = "elasticsearch-test-cluster", - version = new - { - number = "2.0.0", - build_hash = "af1dc6d8099487755c3143c931665b709de3c764", - build_timestamp = "2015-07-07T11:28:47Z", - build_snapshot = true, - lucene_version = "5.2.1" - }, - tagline = "You Know, for Search" - }; - return response; - } + number = "2.0.0", + build_hash = "af1dc6d8099487755c3143c931665b709de3c764", + build_timestamp = "2015-07-07T11:28:47Z", + build_snapshot = true, + lucene_version = "5.2.1" + }, + tagline = "You Know, for Search" + }; + return response; } + } - private void UpdateCluster(VirtualCluster cluster) - { - if (cluster == null) return; + private void UpdateCluster(VirtualCluster cluster) + { + if (cluster == null) return; - lock (Lock) - { - _cluster = cluster; - _calls = cluster.Nodes.ToDictionary(n => n.Uri.Port, v => new State()); - _productRegistration = cluster.ProductRegistration; - } + lock (Lock) + { + _cluster = cluster; + _calls = cluster.Nodes.ToDictionary(n => n.Uri.Port, v => new State()); + _productRegistration = cluster.ProductRegistration; } + } - private bool IsSniffRequest(RequestData requestData) => _productRegistration.IsSniffRequest(requestData); + private bool IsSniffRequest(RequestData requestData) => _productRegistration.IsSniffRequest(requestData); - private bool IsPingRequest(RequestData requestData) => _productRegistration.IsPingRequest(requestData); + private bool IsPingRequest(RequestData requestData) => _productRegistration.IsPingRequest(requestData); - /// > - public override Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) => - Task.FromResult(Request(requestData)); + /// > + public override Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) => + Task.FromResult(Request(requestData)); - /// > - public override TResponse Request(RequestData requestData) - { - if (!_calls.ContainsKey(requestData.Uri.Port)) - throw new Exception($"Expected a call to happen on port {requestData.Uri.Port} but received none"); + /// > + public override TResponse Request(RequestData requestData) + { + if (!_calls.ContainsKey(requestData.Uri.Port)) + throw new Exception($"Expected a call to happen on port {requestData.Uri.Port} but received none"); - try + try + { + var state = _calls[requestData.Uri.Port]; + if (IsSniffRequest(requestData)) { - var state = _calls[requestData.Uri.Port]; - if (IsSniffRequest(requestData)) - { - _ = Interlocked.Increment(ref state.Sniffed); - return HandleRules( - requestData, - nameof(VirtualCluster.Sniff), - _cluster.SniffingRules, - requestData.RequestTimeout, - (r) => UpdateCluster(r.NewClusterState), - (r) => _productRegistration.CreateSniffResponseBytes(_cluster.Nodes, _cluster.ElasticsearchVersion,_cluster.PublishAddressOverride, _cluster.SniffShouldReturnFqnd) - ); - } - if (IsPingRequest(requestData)) - { - _ = Interlocked.Increment(ref state.Pinged); - return HandleRules( - requestData, - nameof(VirtualCluster.Ping), - _cluster.PingingRules, - requestData.PingTimeout, - (r) => { }, - (r) => null //HEAD request - ); - } - _ = Interlocked.Increment(ref state.Called); - return HandleRules( + _ = Interlocked.Increment(ref state.Sniffed); + return HandleRules( requestData, - nameof(VirtualCluster.ClientCalls), - _cluster.ClientCallRules, + nameof(VirtualCluster.Sniff), + _cluster.SniffingRules, requestData.RequestTimeout, - (r) => { }, - CallResponse + (r) => UpdateCluster(r.NewClusterState), + (r) => _productRegistration.CreateSniffResponseBytes(_cluster.Nodes, _cluster.ElasticsearchVersion, _cluster.PublishAddressOverride, _cluster.SniffShouldReturnFqnd) ); } - catch (TheException e) + if (IsPingRequest(requestData)) { - return requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, e, null, null, Stream.Null, null, -1, null, null); + _ = Interlocked.Increment(ref state.Pinged); + return HandleRules( + requestData, + nameof(VirtualCluster.Ping), + _cluster.PingingRules, + requestData.PingTimeout, + (r) => { }, + (r) => null //HEAD request + ); } + _ = Interlocked.Increment(ref state.Called); + return HandleRules( + requestData, + nameof(VirtualCluster.ClientCalls), + _cluster.ClientCallRules, + requestData.RequestTimeout, + (r) => { }, + CallResponse + ); } - - private TResponse HandleRules( - RequestData requestData, - string origin, - IList rules, - TimeSpan timeout, - Action beforeReturn, - Func successResponse - ) - where TResponse : class, ITransportResponse, new() - where TRule : IRule + catch (TheException e) { - requestData.MadeItToResponse = true; - if (rules.Count == 0) - throw new Exception($"No {origin} defined for the current VirtualCluster, so we do not know how to respond"); + return requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, e, null, null, Stream.Null, null, -1, null, null); + } + } - foreach (var rule in rules.Where(s => s.OnPort.HasValue)) - { - var always = rule.Times.Match(t => true, t => false); - var times = rule.Times.Match(t => -1, t => t); + private TResponse HandleRules( + RequestData requestData, + string origin, + IList rules, + TimeSpan timeout, + Action beforeReturn, + Func successResponse + ) + where TResponse : TransportResponse, new() + where TRule : IRule + { + requestData.MadeItToResponse = true; + if (rules.Count == 0) + throw new Exception($"No {origin} defined for the current VirtualCluster, so we do not know how to respond"); - if (rule.OnPort == null || rule.OnPort.Value != requestData.Uri.Port) continue; + foreach (var rule in rules.Where(s => s.OnPort.HasValue)) + { + var always = rule.Times.Match(t => true, t => false); + var times = rule.Times.Match(t => -1, t => t); - if (always) - return Always(requestData, timeout, beforeReturn, successResponse, rule); + if (rule.OnPort == null || rule.OnPort.Value != requestData.Uri.Port) continue; - if (rule.ExecuteCount > times) continue; + if (always) + return Always(requestData, timeout, beforeReturn, successResponse, rule); - return Sometimes(requestData, timeout, beforeReturn, successResponse, rule); - } - foreach (var rule in rules.Where(s => !s.OnPort.HasValue)) - { - var always = rule.Times.Match(t => true, t => false); - var times = rule.Times.Match(t => -1, t => t); - if (always) - return Always(requestData, timeout, beforeReturn, successResponse, rule); + if (rule.ExecuteCount > times) continue; + + return Sometimes(requestData, timeout, beforeReturn, successResponse, rule); + } + foreach (var rule in rules.Where(s => !s.OnPort.HasValue)) + { + var always = rule.Times.Match(t => true, t => false); + var times = rule.Times.Match(t => -1, t => t); + if (always) + return Always(requestData, timeout, beforeReturn, successResponse, rule); - if (rule.ExecuteCount > times) continue; + if (rule.ExecuteCount > times) continue; - return Sometimes(requestData, timeout, beforeReturn, successResponse, rule); - } - var count = _calls.Select(kv => kv.Value.Called).Sum(); - throw new Exception($@"No global or port specific {origin} rule ({requestData.Uri.Port}) matches any longer after {count} calls in to the cluster"); + return Sometimes(requestData, timeout, beforeReturn, successResponse, rule); } + var count = _calls.Select(kv => kv.Value.Called).Sum(); + throw new Exception($@"No global or port specific {origin} rule ({requestData.Uri.Port}) matches any longer after {count} calls in to the cluster"); + } - private TResponse Always(RequestData requestData, TimeSpan timeout, Action beforeReturn, - Func successResponse, TRule rule - ) - where TResponse : class, ITransportResponse, new() - where TRule : IRule + private TResponse Always(RequestData requestData, TimeSpan timeout, Action beforeReturn, + Func successResponse, TRule rule + ) + where TResponse : TransportResponse, new() + where TRule : IRule + { + if (rule.Takes.HasValue) { - if (rule.Takes.HasValue) + var time = timeout < rule.Takes.Value ? timeout : rule.Takes.Value; + _dateTimeProvider.ChangeTime(d => d.Add(time)); + if (rule.Takes.Value > requestData.RequestTimeout) { - var time = timeout < rule.Takes.Value ? timeout : rule.Takes.Value; - _dateTimeProvider.ChangeTime(d => d.Add(time)); - if (rule.Takes.Value > requestData.RequestTimeout) - { - throw new TheException( - $"Request timed out after {time} : call configured to take {rule.Takes.Value} while requestTimeout was: {timeout}"); - } + throw new TheException( + $"Request timed out after {time} : call configured to take {rule.Takes.Value} while requestTimeout was: {timeout}"); } - - return rule.Succeeds - ? Success(requestData, beforeReturn, successResponse, rule) - : Fail(requestData, rule); } - private TResponse Sometimes( - RequestData requestData, TimeSpan timeout, Action beforeReturn, Func successResponse, TRule rule - ) - where TResponse : class, ITransportResponse, new() - where TRule : IRule + return rule.Succeeds + ? Success(requestData, beforeReturn, successResponse, rule) + : Fail(requestData, rule); + } + + private TResponse Sometimes( + RequestData requestData, TimeSpan timeout, Action beforeReturn, Func successResponse, TRule rule + ) + where TResponse : TransportResponse, new() + where TRule : IRule + { + if (rule.Takes.HasValue) { - if (rule.Takes.HasValue) + var time = timeout < rule.Takes.Value ? timeout : rule.Takes.Value; + _dateTimeProvider.ChangeTime(d => d.Add(time)); + if (rule.Takes.Value > requestData.RequestTimeout) { - var time = timeout < rule.Takes.Value ? timeout : rule.Takes.Value; - _dateTimeProvider.ChangeTime(d => d.Add(time)); - if (rule.Takes.Value > requestData.RequestTimeout) - { - throw new TheException( - $"Request timed out after {time} : call configured to take {rule.Takes.Value} while requestTimeout was: {timeout}"); - } + throw new TheException( + $"Request timed out after {time} : call configured to take {rule.Takes.Value} while requestTimeout was: {timeout}"); } - - if (rule.Succeeds) - return Success(requestData, beforeReturn, successResponse, rule); - - return Fail(requestData, rule); } - private TResponse Fail(RequestData requestData, TRule rule, RuleOption returnOverride = null) - where TResponse : class, ITransportResponse, new() - where TRule : IRule - { - var state = _calls[requestData.Uri.Port]; - _ = Interlocked.Increment(ref state.Failures); - var ret = returnOverride ?? rule.Return; - rule.RecordExecuted(); - - if (ret == null) - throw new TheException(); - - return ret.Match( - (e) => throw e, - (statusCode) => ReturnConnectionStatus(requestData, CallResponse(rule), - //make sure we never return a valid status code in Fail responses because of a bad rule. - statusCode >= 200 && statusCode < 300 ? 502 : statusCode, rule.ReturnContentType) - ); - } + if (rule.Succeeds) + return Success(requestData, beforeReturn, successResponse, rule); - private TResponse Success(RequestData requestData, Action beforeReturn, Func successResponse, - TRule rule - ) - where TResponse : class, ITransportResponse, new() - where TRule : IRule - { - var state = _calls[requestData.Uri.Port]; - _ = Interlocked.Increment(ref state.Successes); - rule.RecordExecuted(); + return Fail(requestData, rule); + } - beforeReturn?.Invoke(rule); - return ReturnConnectionStatus(requestData, successResponse(rule), contentType: rule.ReturnContentType); - } + private TResponse Fail(RequestData requestData, TRule rule, RuleOption returnOverride = null) + where TResponse : TransportResponse, new() + where TRule : IRule + { + var state = _calls[requestData.Uri.Port]; + _ = Interlocked.Increment(ref state.Failures); + var ret = returnOverride ?? rule.Return; + rule.RecordExecuted(); + + if (ret == null) + throw new TheException(); + + return ret.Match( + (e) => throw e, + (statusCode) => ReturnConnectionStatus(requestData, CallResponse(rule), + //make sure we never return a valid status code in Fail responses because of a bad rule. + statusCode >= 200 && statusCode < 300 ? 502 : statusCode, rule.ReturnContentType) + ); + } - private static byte[] CallResponse(TRule rule) - where TRule : IRule - { - if (rule?.ReturnResponse != null) - return rule.ReturnResponse; + private TResponse Success(RequestData requestData, Action beforeReturn, Func successResponse, + TRule rule + ) + where TResponse : TransportResponse, new() + where TRule : IRule + { + var state = _calls[requestData.Uri.Port]; + _ = Interlocked.Increment(ref state.Successes); + rule.RecordExecuted(); - if (_defaultResponseBytes != null) return _defaultResponseBytes; + beforeReturn?.Invoke(rule); + return ReturnConnectionStatus(requestData, successResponse(rule), contentType: rule.ReturnContentType); + } - var response = DefaultResponse; - using (var ms = TransportConfiguration.DefaultMemoryStreamFactory.Create()) - { - LowLevelRequestResponseSerializer.Instance.Serialize(response, ms); - _defaultResponseBytes = ms.ToArray(); - } - return _defaultResponseBytes; - } + private static byte[] CallResponse(TRule rule) + where TRule : IRule + { + if (rule?.ReturnResponse != null) + return rule.ReturnResponse; - private class State + if (_defaultResponseBytes != null) return _defaultResponseBytes; + + var response = DefaultResponse; + using (var ms = TransportConfiguration.DefaultMemoryStreamFactory.Create()) { - public int Called; - public int Failures; - public int Pinged; - public int Sniffed; - public int Successes; + LowLevelRequestResponseSerializer.Instance.Serialize(response, ms); + _defaultResponseBytes = ms.ToArray(); } + return _defaultResponseBytes; + } + + private class State + { + public int Called; + public int Failures; + public int Pinged; + public int Sniffed; + public int Successes; } } diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs index 732d6ee..3314d90 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs @@ -7,69 +7,68 @@ using System.Threading.Tasks; using Elastic.Transport.VirtualizedCluster.Providers; -namespace Elastic.Transport.VirtualizedCluster.Components +namespace Elastic.Transport.VirtualizedCluster.Components; + +public class VirtualizedCluster { - public class VirtualizedCluster - { - private readonly ExposingPipelineFactory _exposingRequestPipeline; - private readonly TestableDateTimeProvider _dateTimeProvider; - private readonly TransportConfiguration _settings; + private readonly ExposingPipelineFactory _exposingRequestPipeline; + private readonly TestableDateTimeProvider _dateTimeProvider; + private readonly TransportConfiguration _settings; - private Func, Func, Task> _asyncCall; - private Func, Func, ITransportResponse> _syncCall; + private Func, Func, Task> _asyncCall; + private Func, Func, TransportResponse> _syncCall; - private class VirtualResponse : TransportResponse { } + private class VirtualResponse : TransportResponse { } - internal VirtualizedCluster(TestableDateTimeProvider dateTimeProvider, TransportConfiguration settings) - { - _dateTimeProvider = dateTimeProvider; - _settings = settings; - _exposingRequestPipeline = new ExposingPipelineFactory(settings, _dateTimeProvider); + internal VirtualizedCluster(TestableDateTimeProvider dateTimeProvider, TransportConfiguration settings) + { + _dateTimeProvider = dateTimeProvider; + _settings = settings; + _exposingRequestPipeline = new ExposingPipelineFactory(settings, _dateTimeProvider); - _syncCall = (t, r) => t.Request( + _syncCall = (t, r) => t.Request( + HttpMethod.GET, "/", + PostData.Serializable(new {}), new DefaultRequestParameters() + { + RequestConfiguration = r?.Invoke(new RequestConfigurationDescriptor(null)) + }); + _asyncCall = async (t, r) => + { + var res = await t.RequestAsync + ( HttpMethod.GET, "/", - PostData.Serializable(new {}), new RequestParameters() - { + PostData.Serializable(new { }), + new DefaultRequestParameters() + { RequestConfiguration = r?.Invoke(new RequestConfigurationDescriptor(null)) - }); - _asyncCall = async (t, r) => - { - var res = await t.RequestAsync - ( - HttpMethod.GET, "/", - PostData.Serializable(new { }), - new RequestParameters() - { - RequestConfiguration = r?.Invoke(new RequestConfigurationDescriptor(null)) - }, - CancellationToken.None - ).ConfigureAwait(false); - return (ITransportResponse)res; - }; - } + }, + CancellationToken.None + ).ConfigureAwait(false); + return (TransportResponse)res; + }; + } - public VirtualClusterConnection Connection => Transport.Settings.Connection as VirtualClusterConnection; - public NodePool ConnectionPool => Transport.Settings.NodePool; - public ITransport Transport => _exposingRequestPipeline?.Transport; + public VirtualClusterConnection Connection => Transport.Settings.Connection as VirtualClusterConnection; + public NodePool ConnectionPool => Transport.Settings.NodePool; + public HttpTransport Transport => _exposingRequestPipeline?.Transport; - public VirtualizedCluster TransportProxiesTo( - Func, Func, ITransportResponse> sync, - Func, Func, Task> async - ) - { - _syncCall = sync; - _asyncCall = async; - return this; - } + public VirtualizedCluster TransportProxiesTo( + Func, Func, TransportResponse> sync, + Func, Func, Task> async + ) + { + _syncCall = sync; + _asyncCall = async; + return this; + } - public ITransportResponse ClientCall(Func requestOverrides = null) => - _syncCall(Transport, requestOverrides); + public TransportResponse ClientCall(Func requestOverrides = null) => + _syncCall(Transport, requestOverrides); - public async Task ClientCallAsync(Func requestOverrides = null) => - await _asyncCall(Transport, requestOverrides).ConfigureAwait(false); + public async Task ClientCallAsync(Func requestOverrides = null) => + await _asyncCall(Transport, requestOverrides).ConfigureAwait(false); - public void ChangeTime(Func change) => _dateTimeProvider.ChangeTime(change); + public void ChangeTime(Func change) => _dateTimeProvider.ChangeTime(change); - public void ClientThrows(bool throws) => _settings.ThrowExceptions(throws); - } + public void ClientThrows(bool throws) => _settings.ThrowExceptions(throws); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Elastic.Transport.VirtualizedCluster.csproj b/src/Elastic.Transport.VirtualizedCluster/Elastic.Transport.VirtualizedCluster.csproj index 3722344..152a940 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Elastic.Transport.VirtualizedCluster.csproj +++ b/src/Elastic.Transport.VirtualizedCluster/Elastic.Transport.VirtualizedCluster.csproj @@ -1,7 +1,7 @@ - + Elastic.Transport.VirtualizedCluster - Elastic.Transport.VirtualizedCluster - An in memory ITransportClient that can simulate large cluster + Elastic.Transport.VirtualizedCluster - An in memory TransportClient that can simulate large cluster elasticsearch;elastic;search;lucene;nest Provides a way to assert transport behaviour through a rule engine backed VirtualClusterConnection CS1591;$(NoWarn);IDT001 diff --git a/src/Elastic.Transport.VirtualizedCluster/Extensions/NumericExtensions.cs b/src/Elastic.Transport.VirtualizedCluster/Extensions/NumericExtensions.cs index 7747410..dab3f5d 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Extensions/NumericExtensions.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Extensions/NumericExtensions.cs @@ -2,8 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport.VirtualizedCluster.Extensions -{ +namespace Elastic.Transport.VirtualizedCluster.Extensions; + internal static class NumericExtensions { public static string ToOrdinal(this int num) @@ -31,4 +31,3 @@ public static string ToOrdinal(this int num) } } } -} diff --git a/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchMockProductRegistration.cs b/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchMockProductRegistration.cs index 9082509..5f3c3f5 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchMockProductRegistration.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchMockProductRegistration.cs @@ -7,26 +7,25 @@ using Elastic.Transport.Products; using Elastic.Transport.Products.Elasticsearch; -namespace Elastic.Transport.VirtualizedCluster.Products.Elasticsearch +namespace Elastic.Transport.VirtualizedCluster.Products.Elasticsearch; + +/// > +public sealed class ElasticsearchMockProductRegistration : MockProductRegistration { - /// > - public class ElasticsearchMockProductRegistration : IMockProductRegistration - { - /// A static instance of to reuse - public static IMockProductRegistration Default { get; } = new ElasticsearchMockProductRegistration(); + /// A static instance of to reuse + public static MockProductRegistration Default { get; } = new ElasticsearchMockProductRegistration(); - /// > - public IProductRegistration ProductRegistration { get; } = ElasticsearchProductRegistration.Default; + /// > + public override ProductRegistration ProductRegistration { get; } = ElasticsearchProductRegistration.Default; - /// > - public byte[] CreateSniffResponseBytes(IReadOnlyList nodes, string stackVersion, string publishAddressOverride, bool returnFullyQualifiedDomainNames) => - ElasticsearchSniffResponseFactory.Create(nodes, stackVersion, publishAddressOverride, returnFullyQualifiedDomainNames); + /// > + public override byte[] CreateSniffResponseBytes(IReadOnlyList nodes, string stackVersion, string publishAddressOverride, bool returnFullyQualifiedDomainNames) => + ElasticsearchSniffResponseFactory.Create(nodes, stackVersion, publishAddressOverride, returnFullyQualifiedDomainNames); - public bool IsSniffRequest(RequestData requestData) => - requestData.PathAndQuery.StartsWith(ElasticsearchProductRegistration.SniffPath, StringComparison.Ordinal); + public override bool IsSniffRequest(RequestData requestData) => + requestData.PathAndQuery.StartsWith(ElasticsearchProductRegistration.SniffPath, StringComparison.Ordinal); - public bool IsPingRequest(RequestData requestData) => - requestData.Method == HttpMethod.HEAD && - (requestData.PathAndQuery == string.Empty || requestData.PathAndQuery.StartsWith("?")); - } + public override bool IsPingRequest(RequestData requestData) => + requestData.Method == HttpMethod.HEAD && + (requestData.PathAndQuery == string.Empty || requestData.PathAndQuery.StartsWith("?")); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchSniffResponseFactory.cs b/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchSniffResponseFactory.cs index 3fa941a..cb46dea 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchSniffResponseFactory.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchSniffResponseFactory.cs @@ -7,83 +7,82 @@ using System.Linq; using Elastic.Transport.Products.Elasticsearch; -namespace Elastic.Transport.VirtualizedCluster.Products.Elasticsearch +namespace Elastic.Transport.VirtualizedCluster.Products.Elasticsearch; + +/// A static util method to create an Elasticsearch sniff response. +public static class ElasticsearchSniffResponseFactory { - /// A static util method to create an Elasticsearch sniff response. - public static class ElasticsearchSniffResponseFactory - { - private static string ClusterName => "elasticsearch-test-cluster"; + private static string ClusterName => "elasticsearch-test-cluster"; - /// > - public static byte[] Create(IEnumerable nodes, string elasticsearchVersion,string publishAddressOverride, bool randomFqdn = false) + /// > + public static byte[] Create(IEnumerable nodes, string elasticsearchVersion, string publishAddressOverride, bool randomFqdn = false) + { + var response = new { - var response = new - { - cluster_name = ClusterName, - nodes = SniffResponseNodes(nodes, elasticsearchVersion, publishAddressOverride, randomFqdn) - }; - using (var ms = TransportConfiguration.DefaultMemoryStreamFactory.Create()) - { - LowLevelRequestResponseSerializer.Instance.Serialize(response, ms); - return ms.ToArray(); - } + cluster_name = ClusterName, + nodes = SniffResponseNodes(nodes, elasticsearchVersion, publishAddressOverride, randomFqdn) + }; + using (var ms = TransportConfiguration.DefaultMemoryStreamFactory.Create()) + { + LowLevelRequestResponseSerializer.Instance.Serialize(response, ms); + return ms.ToArray(); } + } - private static IDictionary SniffResponseNodes( - IEnumerable nodes, - string elasticsearchVersion, - string publishAddressOverride, - bool randomFqdn - ) => - (from node in nodes - let id = string.IsNullOrEmpty(node.Id) ? Guid.NewGuid().ToString("N").Substring(0, 8) : node.Id - let name = string.IsNullOrEmpty(node.Name) ? Guid.NewGuid().ToString("N").Substring(0, 8) : node.Name - select new { id, name, node }) - .ToDictionary(kv => kv.id, kv => CreateNodeResponse(kv.node, kv.name, elasticsearchVersion, publishAddressOverride, randomFqdn)); + private static IDictionary SniffResponseNodes( + IEnumerable nodes, + string elasticsearchVersion, + string publishAddressOverride, + bool randomFqdn + ) => + (from node in nodes + let id = string.IsNullOrEmpty(node.Id) ? Guid.NewGuid().ToString("N").Substring(0, 8) : node.Id + let name = string.IsNullOrEmpty(node.Name) ? Guid.NewGuid().ToString("N").Substring(0, 8) : node.Name + select new { id, name, node }) + .ToDictionary(kv => kv.id, kv => CreateNodeResponse(kv.node, kv.name, elasticsearchVersion, publishAddressOverride, randomFqdn)); - private static object CreateNodeResponse(Node node, string name, string elasticsearchVersion, string publishAddressOverride, bool randomFqdn) - { - var port = node.Uri.Port; - var fqdn = randomFqdn ? $"fqdn{port}/" : ""; - var host = !string.IsNullOrWhiteSpace(publishAddressOverride) ? publishAddressOverride : "127.0.0.1"; + private static object CreateNodeResponse(Node node, string name, string elasticsearchVersion, string publishAddressOverride, bool randomFqdn) + { + var port = node.Uri.Port; + var fqdn = randomFqdn ? $"fqdn{port}/" : ""; + var host = !string.IsNullOrWhiteSpace(publishAddressOverride) ? publishAddressOverride : "127.0.0.1"; - var settings = new Dictionary + var settings = new Dictionary { { "cluster.name", ClusterName }, { "node.name", name } }; - foreach (var kv in node.Settings) settings[kv.Key] = kv.Value; + foreach (var kv in node.Settings) settings[kv.Key] = kv.Value; - var httpEnabled = node.HasFeature(ElasticsearchNodeFeatures.HttpEnabled); + var httpEnabled = node.HasFeature(ElasticsearchNodeFeatures.HttpEnabled); - var nodeResponse = new - { - name, - settings, - transport_address = $"127.0.0.1:{port + 1000}]", - host = Guid.NewGuid().ToString("N").Substring(0, 8), - ip = "127.0.0.1", - version = elasticsearchVersion, - build_hash = Guid.NewGuid().ToString("N").Substring(0, 8), - roles = new List(), - http = httpEnabled - ? new + var nodeResponse = new + { + name, + settings, + transport_address = $"127.0.0.1:{port + 1000}]", + host = Guid.NewGuid().ToString("N").Substring(0, 8), + ip = "127.0.0.1", + version = elasticsearchVersion, + build_hash = Guid.NewGuid().ToString("N").Substring(0, 8), + roles = new List(), + http = httpEnabled + ? new + { + bound_address = new[] { - bound_address = new[] - { $"{fqdn}127.0.0.1:{port}" - }, - //publish_address = $"{fqdn}${publishAddress}" - publish_address = $"{fqdn}{host}:{port}" - } - : null - }; - if (node.HasFeature(ElasticsearchNodeFeatures.MasterEligible)) nodeResponse.roles.Add("master"); - if (node.HasFeature(ElasticsearchNodeFeatures.HoldsData)) nodeResponse.roles.Add("data"); - if (node.HasFeature(ElasticsearchNodeFeatures.IngestEnabled)) nodeResponse.roles.Add("ingest"); - if (!httpEnabled) - nodeResponse.settings.Add("http.enabled", false); - return nodeResponse; - } + }, + //publish_address = $"{fqdn}${publishAddress}" + publish_address = $"{fqdn}{host}:{port}" + } + : null + }; + if (node.HasFeature(ElasticsearchNodeFeatures.MasterEligible)) nodeResponse.roles.Add("master"); + if (node.HasFeature(ElasticsearchNodeFeatures.HoldsData)) nodeResponse.roles.Add("data"); + if (node.HasFeature(ElasticsearchNodeFeatures.IngestEnabled)) nodeResponse.roles.Add("ingest"); + if (!httpEnabled) + nodeResponse.settings.Add("http.enabled", false); + return nodeResponse; } } diff --git a/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchVirtualCluster.cs b/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchVirtualCluster.cs index b89db53..021c060 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchVirtualCluster.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Products/Elasticsearch/ElasticsearchVirtualCluster.cs @@ -9,98 +9,95 @@ using Elastic.Transport.Products.Elasticsearch; using Elastic.Transport.VirtualizedCluster.Components; -namespace Elastic.Transport.VirtualizedCluster.Products.Elasticsearch +namespace Elastic.Transport.VirtualizedCluster.Products.Elasticsearch; + +/// +/// Bootstrap an elasticsearch virtual cluster. This class can not be instantiated directly, use instead. +/// +public sealed class ElasticsearchClusterFactory { + internal static ElasticsearchClusterFactory Default { get; } = new ElasticsearchClusterFactory(); + + private ElasticsearchClusterFactory() { } + + // ReSharper disable once MemberCanBeMadeStatic.Global /// - /// Bootstrap an elasticsearch virtual cluster. This class can not be instantiated directly, use instead. + /// Bootstrap a cluster with . By default all clusters start their nodes at 9200. + /// You can provide a different starting number using . /// - public class ElasticsearchClusterFactory - { - internal static ElasticsearchClusterFactory Default { get; } = new ElasticsearchClusterFactory(); - - private ElasticsearchClusterFactory() { } + public ElasticsearchVirtualCluster Bootstrap(int numberOfNodes, int startFrom = 9200) => + new( + Enumerable.Range(startFrom, numberOfNodes).Select(n => new Node(new Uri($"http://localhost:{n}"))) + ); - // ReSharper disable once MemberCanBeMadeStatic.Global - /// - /// Bootstrap a cluster with . By default all clusters start their nodes at 9200. - /// You can provide a different starting number using . - /// - public ElasticsearchVirtualCluster Bootstrap(int numberOfNodes, int startFrom = 9200) => - new ElasticsearchVirtualCluster( - Enumerable.Range(startFrom, numberOfNodes).Select(n => new Node(new Uri($"http://localhost:{n}"))) - ); + // ReSharper disable once MemberCanBeMadeStatic.Global + /// Bootstrap a cluster by providing the nodes explicitly + public ElasticsearchVirtualCluster Bootstrap(IEnumerable nodes) => new(nodes); - // ReSharper disable once MemberCanBeMadeStatic.Global - /// Bootstrap a cluster by providing the nodes explicitly - public ElasticsearchVirtualCluster Bootstrap(IEnumerable nodes) => new ElasticsearchVirtualCluster(nodes); + // ReSharper disable once MemberCanBeMadeStatic.Global + /// + /// Bootstrap a cluster with . By default all clusters start their nodes at 9200. + /// You can provide a different starting number using . + /// Using this overload all the nodes in the cluster are ONLY master eligible + /// + public ElasticsearchVirtualCluster BootstrapAllMasterEligableOnly(int numberOfNodes, int startFrom = 9200) => + new( + Enumerable.Range(startFrom, numberOfNodes) + .Select(n => new Node(new Uri($"http://localhost:{n}"), ElasticsearchNodeFeatures.MasterEligibleOnly) + ) + ); +} - // ReSharper disable once MemberCanBeMadeStatic.Global - /// - /// Bootstrap a cluster with . By default all clusters start their nodes at 9200. - /// You can provide a different starting number using . - /// Using this overload all the nodes in the cluster are ONLY master eligible - /// - public ElasticsearchVirtualCluster BootstrapAllMasterEligableOnly(int numberOfNodes, int startFrom = 9200) => - new ElasticsearchVirtualCluster( - Enumerable.Range(startFrom, numberOfNodes) - .Select(n => new Node(new Uri($"http://localhost:{n}"), ElasticsearchNodeFeatures.MasterEligibleOnly) - ) - ); - } +/// +/// Create a virtual Elasticsearch cluster by passing a list of . +/// Please see for a more convenient pattern to create an instance of this class. +/// +public class ElasticsearchVirtualCluster : VirtualCluster +{ + /// > + public ElasticsearchVirtualCluster(IEnumerable nodes) : base(nodes, ElasticsearchMockProductRegistration.Default) { } /// - /// Create a virtual Elasticsearch cluster by passing a list of . - /// Please see for a more convenient pattern to create an instance of this class. + /// Makes sure **only** the nodes with the passed port numbers are marked as master eligible. By default **all** nodes + /// are master eligible. /// - public class ElasticsearchVirtualCluster : VirtualCluster + public ElasticsearchVirtualCluster MasterEligible(params int[] ports) { - /// > - public ElasticsearchVirtualCluster(IEnumerable nodes) : base(nodes, ElasticsearchMockProductRegistration.Default) { } - - /// - /// Makes sure **only** the nodes with the passed port numbers are marked as master eligible. By default **all** nodes - /// are master eligible. - /// - public ElasticsearchVirtualCluster MasterEligible(params int[] ports) + foreach (var node in InternalNodes.Where(n => !ports.Contains(n.Uri.Port))) { - foreach (var node in InternalNodes.Where(n => !ports.Contains(n.Uri.Port))) - { - var currentFeatures = node.Features.Count == 0 ? ElasticsearchNodeFeatures.Default : node.Features; - node.Features = currentFeatures.Except(new[] { ElasticsearchNodeFeatures.MasterEligible }).ToList().AsReadOnly(); - } - return this; - } - - /// Removes the data role from the nodes with the passed port numbers - public ElasticsearchVirtualCluster StoresNoData(params int[] ports) - { - foreach (var node in InternalNodes.Where(n => ports.Contains(n.Uri.Port))) - { - var currentFeatures = node.Features.Count == 0 ? ElasticsearchNodeFeatures.Default : node.Features; - node.Features = currentFeatures.Except(new[] { ElasticsearchNodeFeatures.HoldsData }).ToList().AsReadOnly(); - } - return this; + var currentFeatures = node.Features.Count == 0 ? ElasticsearchNodeFeatures.Default : node.Features; + node.Features = currentFeatures.Except(new[] { ElasticsearchNodeFeatures.MasterEligible }).ToList().AsReadOnly(); } + return this; + } - /// Disables http on the nodes with the passed port numbers - public VirtualCluster HttpDisabled(params int[] ports) + /// Removes the data role from the nodes with the passed port numbers + public ElasticsearchVirtualCluster StoresNoData(params int[] ports) + { + foreach (var node in InternalNodes.Where(n => ports.Contains(n.Uri.Port))) { - foreach (var node in InternalNodes.Where(n => ports.Contains(n.Uri.Port))) - { - var currentFeatures = node.Features.Count == 0 ? ElasticsearchNodeFeatures.Default : node.Features; - node.Features = currentFeatures.Except(new[] { ElasticsearchNodeFeatures.HttpEnabled }).ToList().AsReadOnly(); - } - return this; + var currentFeatures = node.Features.Count == 0 ? ElasticsearchNodeFeatures.Default : node.Features; + node.Features = currentFeatures.Except(new[] { ElasticsearchNodeFeatures.HoldsData }).ToList().AsReadOnly(); } + return this; + } - /// Add a setting to the nodes with the passed port numbers - public ElasticsearchVirtualCluster HasSetting(string key, string value, params int[] ports) + /// Disables http on the nodes with the passed port numbers + public VirtualCluster HttpDisabled(params int[] ports) + { + foreach (var node in InternalNodes.Where(n => ports.Contains(n.Uri.Port))) { - foreach (var node in InternalNodes.Where(n => ports.Contains(n.Uri.Port))) - node.Settings = new ReadOnlyDictionary(new Dictionary { { key, value } }); - return this; + var currentFeatures = node.Features.Count == 0 ? ElasticsearchNodeFeatures.Default : node.Features; + node.Features = currentFeatures.Except(new[] { ElasticsearchNodeFeatures.HttpEnabled }).ToList().AsReadOnly(); } + return this; + } - + /// Add a setting to the nodes with the passed port numbers + public ElasticsearchVirtualCluster HasSetting(string key, string value, params int[] ports) + { + foreach (var node in InternalNodes.Where(n => ports.Contains(n.Uri.Port))) + node.Settings = new ReadOnlyDictionary(new Dictionary { { key, value } }); + return this; } } diff --git a/src/Elastic.Transport.VirtualizedCluster/Products/IMockProductRegistration.cs b/src/Elastic.Transport.VirtualizedCluster/Products/IMockProductRegistration.cs deleted file mode 100644 index 4347ccc..0000000 --- a/src/Elastic.Transport.VirtualizedCluster/Products/IMockProductRegistration.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Collections.Generic; -using Elastic.Transport.Products; -using Elastic.Transport.VirtualizedCluster.Components; - -namespace Elastic.Transport.VirtualizedCluster.Products -{ - /// - /// Makes sure is mockable by providing a different sniff response based on the current - /// - public interface IMockProductRegistration - { - /// - /// Information about the current product we are injecting into - /// - IProductRegistration ProductRegistration { get; } - - /// - /// Return the sniff response for the product as raw bytes for to return. - /// - /// The nodes we expect to be returned in the response - /// The current version under test - /// Return this hostname instead of some IP - /// If the sniff can return internal + external information return both - byte[] CreateSniffResponseBytes(IReadOnlyList nodes, string stackVersion, string publishAddressOverride, bool returnFullyQualifiedDomainNames); - - /// - /// see uses this to determine if the current request is a sniff request and should follow - /// the sniffing rules - /// - bool IsSniffRequest(RequestData requestData); - - bool IsPingRequest(RequestData requestData); - } -} diff --git a/src/Elastic.Transport.VirtualizedCluster/Products/MockProductRegistration.cs b/src/Elastic.Transport.VirtualizedCluster/Products/MockProductRegistration.cs new file mode 100644 index 0000000..f54647c --- /dev/null +++ b/src/Elastic.Transport.VirtualizedCluster/Products/MockProductRegistration.cs @@ -0,0 +1,37 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Generic; +using Elastic.Transport.Products; +using Elastic.Transport.VirtualizedCluster.Components; + +namespace Elastic.Transport.VirtualizedCluster.Products; + +/// +/// Makes sure is mockable by providing a different sniff response based on the current +/// +public abstract class MockProductRegistration +{ + /// + /// Information about the current product we are injecting into + /// + public abstract ProductRegistration ProductRegistration { get; } + + /// + /// Return the sniff response for the product as raw bytes for to return. + /// + /// The nodes we expect to be returned in the response + /// The current version under test + /// Return this hostname instead of some IP + /// If the sniff can return internal + external information return both + public abstract byte[] CreateSniffResponseBytes(IReadOnlyList nodes, string stackVersion, string publishAddressOverride, bool returnFullyQualifiedDomainNames); + + /// + /// see uses this to determine if the current request is a sniff request and should follow + /// the sniffing rules + /// + public abstract bool IsSniffRequest(RequestData requestData); + + public abstract bool IsPingRequest(RequestData requestData); +} diff --git a/src/Elastic.Transport.VirtualizedCluster/Providers/TestableDateTimeProvider.cs b/src/Elastic.Transport.VirtualizedCluster/Providers/TestableDateTimeProvider.cs index 762704c..054f5fb 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Providers/TestableDateTimeProvider.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Providers/TestableDateTimeProvider.cs @@ -4,20 +4,21 @@ using System; -namespace Elastic.Transport.VirtualizedCluster.Providers +namespace Elastic.Transport.VirtualizedCluster.Providers; + +/// +public sealed class TestableDateTimeProvider : DateTimeProvider { - /// - public class TestableDateTimeProvider : DateTimeProvider - { - private DateTime MutableNow { get; set; } = DateTime.UtcNow; + private DateTime MutableNow { get; set; } = DateTime.UtcNow; + + /// + public override DateTime Now() => MutableNow; - /// - public override DateTime Now() => MutableNow; + /// + /// Advance the time returns + /// + /// A fun that gets passed the current and needs to return the new value + public void ChangeTime(Func change) => MutableNow = change(MutableNow); - /// - /// Advance the time returns - /// - /// A fun that gets passed the current and needs to return the new value - public void ChangeTime(Func change) => MutableNow = change(MutableNow); - } + public override DateTime DeadTime(int attempts, TimeSpan? minDeadTimeout, TimeSpan? maxDeadTimeout) => throw new NotImplementedException(); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Rules/ClientCallRule.cs b/src/Elastic.Transport.VirtualizedCluster/Rules/ClientCallRule.cs index f6c9dad..820f79f 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Rules/ClientCallRule.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Rules/ClientCallRule.cs @@ -2,51 +2,50 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System; #if DOTNETCORE +using System; using TheException = System.Net.Http.HttpRequestException; #else using TheException = System.Net.WebException; #endif -namespace Elastic.Transport.VirtualizedCluster.Rules +namespace Elastic.Transport.VirtualizedCluster.Rules; + +public interface IClientCallRule : IRule { } + +public sealed class ClientCallRule : RuleBase, IClientCallRule { - public interface IClientCallRule : IRule { } + private IClientCallRule Self => this; - public class ClientCallRule : RuleBase, IClientCallRule + public ClientCallRule Fails(RuleOption times, RuleOption errorState = null) { - private IClientCallRule Self => this; - - public ClientCallRule Fails(RuleOption times, RuleOption errorState = null) - { - Self.Times = times; - Self.Succeeds = false; - Self.Return = errorState ?? new TheException(); - return this; - } - - public ClientCallRule Succeeds(RuleOption times, int? validResponseCode = 200) - { - Self.Times = times; - Self.Succeeds = true; - Self.Return = validResponseCode; - return this; - } - - public ClientCallRule AfterSucceeds(RuleOption errorState = null) - { - Self.AfterSucceeds = errorState; - return this; - } - - public ClientCallRule ThrowsAfterSucceeds() - { - Self.AfterSucceeds = new TheException(); - return this; - } - - public ClientCallRule SucceedAlways(int? validResponseCode = 200) => Succeeds(TimesHelper.Always, validResponseCode); - - public ClientCallRule FailAlways(RuleOption errorState = null) => Fails(TimesHelper.Always, errorState); + Self.Times = times; + Self.Succeeds = false; + Self.Return = errorState ?? new TheException(); + return this; } + + public ClientCallRule Succeeds(RuleOption times, int? validResponseCode = 200) + { + Self.Times = times; + Self.Succeeds = true; + Self.Return = validResponseCode; + return this; + } + + public ClientCallRule AfterSucceeds(RuleOption errorState = null) + { + Self.AfterSucceeds = errorState; + return this; + } + + public ClientCallRule ThrowsAfterSucceeds() + { + Self.AfterSucceeds = new TheException(); + return this; + } + + public ClientCallRule SucceedAlways(int? validResponseCode = 200) => Succeeds(TimesHelper.Always, validResponseCode); + + public ClientCallRule FailAlways(RuleOption errorState = null) => Fails(TimesHelper.Always, errorState); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Rules/PingRule.cs b/src/Elastic.Transport.VirtualizedCluster/Rules/PingRule.cs index 0e23584..a9c5d0b 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Rules/PingRule.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Rules/PingRule.cs @@ -4,30 +4,29 @@ using System; -namespace Elastic.Transport.VirtualizedCluster.Rules +namespace Elastic.Transport.VirtualizedCluster.Rules; + +public sealed class PingRule : RuleBase { - public class PingRule : RuleBase - { - private IRule Self => this; + private IRule Self => this; - public PingRule Fails(RuleOption times, RuleOption errorState = null) - { - Self.Times = times; - Self.Succeeds = false; - Self.Return = errorState; - return this; - } + public PingRule Fails(RuleOption times, RuleOption errorState = null) + { + Self.Times = times; + Self.Succeeds = false; + Self.Return = errorState; + return this; + } - public PingRule Succeeds(RuleOption times, int? validResponseCode = 200) - { - Self.Times = times; - Self.Succeeds = true; - Self.Return = validResponseCode; - return this; - } + public PingRule Succeeds(RuleOption times, int? validResponseCode = 200) + { + Self.Times = times; + Self.Succeeds = true; + Self.Return = validResponseCode; + return this; + } - public PingRule SucceedAlways(int? validResponseCode = 200) => Succeeds(TimesHelper.Always, validResponseCode); + public PingRule SucceedAlways(int? validResponseCode = 200) => Succeeds(TimesHelper.Always, validResponseCode); - public PingRule FailAlways(RuleOption errorState = null) => Fails(TimesHelper.Always, errorState); - } + public PingRule FailAlways(RuleOption errorState = null) => Fails(TimesHelper.Always, errorState); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Rules/RuleBase.cs b/src/Elastic.Transport.VirtualizedCluster/Rules/RuleBase.cs index 2deb967..855d497 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Rules/RuleBase.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Rules/RuleBase.cs @@ -2,93 +2,92 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information - using System; - using System.Threading; +using System; +using System.Threading; -namespace Elastic.Transport.VirtualizedCluster.Rules +namespace Elastic.Transport.VirtualizedCluster.Rules; + +public interface IRule { - public interface IRule - { - /// The value or exception to return after the call succeeds - RuleOption AfterSucceeds { get; set; } + /// The value or exception to return after the call succeeds + RuleOption AfterSucceeds { get; set; } - /// This rule is constrain on the node with this port number - int? OnPort { get; set; } + /// This rule is constrain on the node with this port number + int? OnPort { get; set; } - /// Either a hard exception or soft HTTP error code - RuleOption Return { get; set; } + /// Either a hard exception or soft HTTP error code + RuleOption Return { get; set; } - /// Set an explicit return content type for the API call - string ReturnContentType { get; set; } + /// Set an explicit return content type for the API call + string ReturnContentType { get; set; } - /// Explicitly set the bytes returned by the API call, optional. - byte[] ReturnResponse { get; set; } + /// Explicitly set the bytes returned by the API call, optional. + byte[] ReturnResponse { get; set; } - /// Whether this rule describes an API call succeeding or not - bool Succeeds { get; set; } + /// Whether this rule describes an API call succeeding or not + bool Succeeds { get; set; } - /// Simulate a long running call - TimeSpan? Takes { get; set; } + /// Simulate a long running call + TimeSpan? Takes { get; set; } - /// The number of times this rule stays valid after being called - RuleOption Times { get; set; } + /// The number of times this rule stays valid after being called + RuleOption Times { get; set; } - /// The amount of times this rule has been executed - int ExecuteCount { get; } + /// The amount of times this rule has been executed + int ExecuteCount { get; } - /// Mark a rule as executed - void RecordExecuted(); - } + /// Mark a rule as executed + void RecordExecuted(); +} - public abstract class RuleBase : IRule - where TRule : RuleBase, IRule +public abstract class RuleBase : IRule + where TRule : RuleBase, IRule +{ + private int _executeCount; + RuleOption IRule.AfterSucceeds { get; set; } + int? IRule.OnPort { get; set; } + RuleOption IRule.Return { get; set; } + string IRule.ReturnContentType { get; set; } + byte[] IRule.ReturnResponse { get; set; } + private IRule Self => this; + bool IRule.Succeeds { get; set; } + TimeSpan? IRule.Takes { get; set; } + RuleOption IRule.Times { get; set; } + + int IRule.ExecuteCount => _executeCount; + + void IRule.RecordExecuted() => Interlocked.Increment(ref _executeCount); + + public TRule OnPort(int port) { - private int _executeCount; - RuleOption IRule.AfterSucceeds { get; set; } - int? IRule.OnPort { get; set; } - RuleOption IRule.Return { get; set; } - string IRule.ReturnContentType { get; set; } - byte[] IRule.ReturnResponse { get; set; } - private IRule Self => this; - bool IRule.Succeeds { get; set; } - TimeSpan? IRule.Takes { get; set; } - RuleOption IRule.Times { get; set; } - - int IRule.ExecuteCount => _executeCount; - - void IRule.RecordExecuted() => Interlocked.Increment(ref _executeCount); - - public TRule OnPort(int port) - { - Self.OnPort = port; - return (TRule)this; - } + Self.OnPort = port; + return (TRule)this; + } - public TRule Takes(TimeSpan span) - { - Self.Takes = span; - return (TRule)this; - } + public TRule Takes(TimeSpan span) + { + Self.Takes = span; + return (TRule)this; + } - public TRule ReturnResponse(T response) - where T : class + public TRule ReturnResponse(T response) + where T : class + { + byte[] r; + using (var ms = TransportConfiguration.DefaultMemoryStreamFactory.Create()) { - byte[] r; - using (var ms = TransportConfiguration.DefaultMemoryStreamFactory.Create()) - { - LowLevelRequestResponseSerializer.Instance.Serialize(response, ms); - r = ms.ToArray(); - } - Self.ReturnResponse = r; - Self.ReturnContentType = RequestData.MimeType; - return (TRule)this; + LowLevelRequestResponseSerializer.Instance.Serialize(response, ms); + r = ms.ToArray(); } + Self.ReturnResponse = r; + Self.ReturnContentType = RequestData.MimeType; + return (TRule)this; + } - public TRule ReturnByteResponse(byte[] response, string responseContentType = RequestData.MimeType) - { - Self.ReturnResponse = response; - Self.ReturnContentType = responseContentType; - return (TRule)this; - } + public TRule ReturnByteResponse(byte[] response, string responseContentType = RequestData.MimeType) + { + Self.ReturnResponse = response; + Self.ReturnContentType = responseContentType; + return (TRule)this; } } diff --git a/src/Elastic.Transport.VirtualizedCluster/Rules/RuleOption.cs b/src/Elastic.Transport.VirtualizedCluster/Rules/RuleOption.cs index 0aaae1b..fd24718 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Rules/RuleOption.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Rules/RuleOption.cs @@ -4,76 +4,75 @@ using System; -namespace Elastic.Transport.VirtualizedCluster.Rules +namespace Elastic.Transport.VirtualizedCluster.Rules; + +/// +/// Represents the union of two types, and . +/// Used to represent a rule with multiple return states +/// +/// The first type +/// The second type +public sealed class RuleOption { + internal readonly int Tag; + internal readonly TFirst Item1; + internal readonly TSecond Item2; + /// - /// Represents the union of two types, and . - /// Used to represent a rule with multiple return states + /// Creates an new instance of that encapsulates value /// - /// The first type - /// The second type - public class RuleOption + /// The value to encapsulate + public RuleOption(TFirst item) { - internal readonly int Tag; - internal readonly TFirst Item1; - internal readonly TSecond Item2; - - /// - /// Creates an new instance of that encapsulates value - /// - /// The value to encapsulate - public RuleOption(TFirst item) - { - Item1 = item; - Tag = 0; - } + Item1 = item; + Tag = 0; + } - /// - /// Creates an new instance of that encapsulates value - /// - /// The value to encapsulate - public RuleOption(TSecond item) - { - Item2 = item; - Tag = 1; - } + /// + /// Creates an new instance of that encapsulates value + /// + /// The value to encapsulate + public RuleOption(TSecond item) + { + Item2 = item; + Tag = 1; + } - /// - /// Runs an delegate against the encapsulated value - /// - /// The delegate to run when this instance encapsulates an instance of - /// The delegate to run when this instance encapsulates an instance of - public void Match(Action first, Action second) + /// + /// Runs an delegate against the encapsulated value + /// + /// The delegate to run when this instance encapsulates an instance of + /// The delegate to run when this instance encapsulates an instance of + public void Match(Action first, Action second) + { + switch (Tag) { - switch (Tag) - { - case 0: - first(Item1); - break; - case 1: - second(Item2); - break; - default: throw new Exception($"Unrecognized tag value: {Tag}"); - } + case 0: + first(Item1); + break; + case 1: + second(Item2); + break; + default: throw new Exception($"Unrecognized tag value: {Tag}"); } + } - /// - /// Runs a delegate against the encapsulated value - /// - /// The delegate to run when this instance encapsulates an instance of - /// The delegate to run when this instance encapsulates an instance of - public T Match(Func first, Func second) + /// + /// Runs a delegate against the encapsulated value + /// + /// The delegate to run when this instance encapsulates an instance of + /// The delegate to run when this instance encapsulates an instance of + public T Match(Func first, Func second) + { + switch (Tag) { - switch (Tag) - { - case 0: return first(Item1); - case 1: return second(Item2); - default: throw new Exception($"Unrecognized tag value: {Tag}"); - } + case 0: return first(Item1); + case 1: return second(Item2); + default: throw new Exception($"Unrecognized tag value: {Tag}"); } + } - public static implicit operator RuleOption(TFirst first) => new RuleOption(first); + public static implicit operator RuleOption(TFirst first) => new(first); - public static implicit operator RuleOption(TSecond second) => new RuleOption(second); - } + public static implicit operator RuleOption(TSecond second) => new(second); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Rules/SniffRule.cs b/src/Elastic.Transport.VirtualizedCluster/Rules/SniffRule.cs index 0b73100..60b9a19 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Rules/SniffRule.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Rules/SniffRule.cs @@ -5,38 +5,37 @@ using System; using Elastic.Transport.VirtualizedCluster.Components; -namespace Elastic.Transport.VirtualizedCluster.Rules +namespace Elastic.Transport.VirtualizedCluster.Rules; + +public interface ISniffRule : IRule +{ + /// The new cluster state after the sniff returns + VirtualCluster NewClusterState { get; set; } +} + +public sealed class SniffRule : RuleBase, ISniffRule { - public interface ISniffRule : IRule + VirtualCluster ISniffRule.NewClusterState { get; set; } + private ISniffRule Self => this; + + public SniffRule Fails(RuleOption times, RuleOption errorState = null) { - /// The new cluster state after the sniff returns - VirtualCluster NewClusterState { get; set; } + Self.Times = times; + Self.Succeeds = false; + Self.Return = errorState; + return this; } - public class SniffRule : RuleBase, ISniffRule + public SniffRule Succeeds(RuleOption times, VirtualCluster cluster = null) { - VirtualCluster ISniffRule.NewClusterState { get; set; } - private ISniffRule Self => this; - - public SniffRule Fails(RuleOption times, RuleOption errorState = null) - { - Self.Times = times; - Self.Succeeds = false; - Self.Return = errorState; - return this; - } - - public SniffRule Succeeds(RuleOption times, VirtualCluster cluster = null) - { - Self.Times = times; - Self.Succeeds = true; - Self.NewClusterState = cluster; - Self.Return = 200; - return this; - } - - public SniffRule SucceedAlways(VirtualCluster cluster = null) => Succeeds(TimesHelper.Always, cluster); - - public SniffRule FailAlways(RuleOption errorState = null) => Fails(TimesHelper.Always, errorState); + Self.Times = times; + Self.Succeeds = true; + Self.NewClusterState = cluster; + Self.Return = 200; + return this; } + + public SniffRule SucceedAlways(VirtualCluster cluster = null) => Succeeds(TimesHelper.Always, cluster); + + public SniffRule FailAlways(RuleOption errorState = null) => Fails(TimesHelper.Always, errorState); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Rules/TimesHelper.cs b/src/Elastic.Transport.VirtualizedCluster/Rules/TimesHelper.cs index 3f7cddc..d47d64d 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Rules/TimesHelper.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Rules/TimesHelper.cs @@ -4,19 +4,18 @@ using System; -namespace Elastic.Transport.VirtualizedCluster.Rules +namespace Elastic.Transport.VirtualizedCluster.Rules; + +public static class TimesHelper { - public static class TimesHelper - { - public static AllTimes Always = new AllTimes(); - public static readonly int Once = 0; - public static readonly int Twice = 1; + public static AllTimes Always = new(); + public static readonly int Once = 0; + public static readonly int Twice = 1; - public static int Times(int n) => Math.Max(0, n - 1); + public static int Times(int n) => Math.Max(0, n - 1); - public class AllTimes - { - internal AllTimes() { } - } + public class AllTimes + { + internal AllTimes() { } } } diff --git a/src/Elastic.Transport.VirtualizedCluster/Setup.cs b/src/Elastic.Transport.VirtualizedCluster/Setup.cs index cdc86f7..f48287f 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Setup.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Setup.cs @@ -4,8 +4,8 @@ using Elastic.Transport.VirtualizedCluster.Products.Elasticsearch; -namespace Elastic.Transport.VirtualizedCluster -{ +namespace Elastic.Transport.VirtualizedCluster; + /// /// Static factory class that can be used to bootstrap virtual product clusters. E.g a cluster of virtual Elasticsearch nodes. /// @@ -16,4 +16,3 @@ public static class Virtual /// public static ElasticsearchClusterFactory Elasticsearch { get; } = ElasticsearchClusterFactory.Default; } -} diff --git a/src/Elastic.Transport/Components/NodePool/CloudNodePool.cs b/src/Elastic.Transport/Components/NodePool/CloudNodePool.cs index c7b2413..54282d9 100644 --- a/src/Elastic.Transport/Components/NodePool/CloudNodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/CloudNodePool.cs @@ -5,95 +5,94 @@ using System; using System.Text; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// An implementation that can be seeded with a cloud id +/// and will signal the right defaults for the client to use for Elastic Cloud to . +/// +/// Read more about Elastic Cloud Id: +/// https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html +/// +public sealed class CloudNodePool : SingleNodePool { /// /// An implementation that can be seeded with a cloud id /// and will signal the right defaults for the client to use for Elastic Cloud to . /// - /// Read more about Elastic Cloud Id: + /// Read more about Elastic Cloud Id here /// https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html /// - public sealed class CloudNodePool : SingleNodePool + /// + /// The Cloud Id, this is available on your cluster's dashboard and is a string in the form of cluster_name:base_64_encoded_string + /// Base64 encoded string contains the following tokens in order separated by $: + /// * Host Name (mandatory) + /// * Elasticsearch UUID (mandatory) + /// * Kibana UUID + /// * APM UUID + /// + /// We then use these tokens to create the URI to your Elastic Cloud cluster! + /// + /// Read more here: https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html + /// + /// + /// Optionally inject an instance of used to set + public CloudNodePool(string cloudId, AuthorizationHeader credentials, DateTimeProvider dateTimeProvider = null) : this(ParseCloudId(cloudId), dateTimeProvider) => + AuthenticationHeader = credentials; + + private CloudNodePool(ParsedCloudId parsedCloudId, DateTimeProvider dateTimeProvider = null) : base(parsedCloudId.Uri, dateTimeProvider) => + ClusterName = parsedCloudId.Name; + + //TODO implement debugger display for NodePool implementations and display it there and its ToString() + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private string ClusterName { get; } + + /// + public AuthorizationHeader AuthenticationHeader { get; } + + private readonly struct ParsedCloudId { - /// - /// An implementation that can be seeded with a cloud id - /// and will signal the right defaults for the client to use for Elastic Cloud to . - /// - /// Read more about Elastic Cloud Id here - /// https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html - /// - /// - /// The Cloud Id, this is available on your cluster's dashboard and is a string in the form of cluster_name:base_64_encoded_string - /// Base64 encoded string contains the following tokens in order separated by $: - /// * Host Name (mandatory) - /// * Elasticsearch UUID (mandatory) - /// * Kibana UUID - /// * APM UUID - /// - /// We then use these tokens to create the URI to your Elastic Cloud cluster! - /// - /// Read more here: https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html - /// - /// - /// Optionally inject an instance of used to set - public CloudNodePool(string cloudId, AuthorizationHeader credentials, IDateTimeProvider dateTimeProvider = null) : this(ParseCloudId(cloudId), dateTimeProvider) => - AuthenticationHeader = credentials; - - private CloudNodePool(ParsedCloudId parsedCloudId, IDateTimeProvider dateTimeProvider = null) : base(parsedCloudId.Uri, dateTimeProvider) => - ClusterName = parsedCloudId.Name; - - //TODO implement debugger display for NodePool implementations and display it there and its ToString() - // ReSharper disable once UnusedAutoPropertyAccessor.Local - private string ClusterName { get; } - - /// - public AuthorizationHeader AuthenticationHeader { get; } - - private readonly struct ParsedCloudId + public ParsedCloudId(string clusterName, Uri uri) { - public ParsedCloudId(string clusterName, Uri uri) - { - Name = clusterName; - Uri = uri; - } - - public string Name { get; } - public Uri Uri { get; } + Name = clusterName; + Uri = uri; } - private static ParsedCloudId ParseCloudId(string cloudId) - { - const string exceptionSuffix = "should be a string in the form of cluster_name:base_64_data"; - if (string.IsNullOrWhiteSpace(cloudId)) - throw new ArgumentException($"Parameter {nameof(cloudId)} was null or empty but {exceptionSuffix}", nameof(cloudId)); + public string Name { get; } + public Uri Uri { get; } + } - var tokens = cloudId.Split(new[] { ':' }, 2); - if (tokens.Length != 2) - throw new ArgumentException($"Parameter {nameof(cloudId)} not in expected format, {exceptionSuffix}", nameof(cloudId)); + private static ParsedCloudId ParseCloudId(string cloudId) + { + const string exceptionSuffix = "should be a string in the form of cluster_name:base_64_data"; + if (string.IsNullOrWhiteSpace(cloudId)) + throw new ArgumentException($"Parameter {nameof(cloudId)} was null or empty but {exceptionSuffix}", nameof(cloudId)); - var clusterName = tokens[0]; - var encoded = tokens[1]; - if (string.IsNullOrWhiteSpace(encoded)) - throw new ArgumentException($"Parameter {nameof(cloudId)} base_64_data is empty, {exceptionSuffix}", nameof(cloudId)); + var tokens = cloudId.Split(new[] { ':' }, 2); + if (tokens.Length != 2) + throw new ArgumentException($"Parameter {nameof(cloudId)} not in expected format, {exceptionSuffix}", nameof(cloudId)); - var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); - var parts = decoded.Split(new[] { '$' }); - if (parts.Length < 2) - throw new ArgumentException($"Parameter {nameof(cloudId)} decoded base_64_data contains less then 2 tokens, {exceptionSuffix}", nameof(cloudId)); + var clusterName = tokens[0]; + var encoded = tokens[1]; + if (string.IsNullOrWhiteSpace(encoded)) + throw new ArgumentException($"Parameter {nameof(cloudId)} base_64_data is empty, {exceptionSuffix}", nameof(cloudId)); - var domainName = parts[0].Trim(); - if (string.IsNullOrWhiteSpace(domainName)) - throw new ArgumentException($"Parameter {nameof(cloudId)} decoded base_64_data contains no domain name, {exceptionSuffix}", nameof(cloudId)); + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); + var parts = decoded.Split(new[] { '$' }); + if (parts.Length < 2) + throw new ArgumentException($"Parameter {nameof(cloudId)} decoded base_64_data contains less then 2 tokens, {exceptionSuffix}", nameof(cloudId)); - var elasticsearchUuid = parts[1].Trim(); - if (string.IsNullOrWhiteSpace(elasticsearchUuid)) - throw new ArgumentException($"Parameter {nameof(cloudId)} decoded base_64_data contains no elasticsearch UUID, {exceptionSuffix}", nameof(cloudId)); + var domainName = parts[0].Trim(); + if (string.IsNullOrWhiteSpace(domainName)) + throw new ArgumentException($"Parameter {nameof(cloudId)} decoded base_64_data contains no domain name, {exceptionSuffix}", nameof(cloudId)); - return new ParsedCloudId(clusterName, new Uri($"https://{elasticsearchUuid}.{domainName}")); - } + var elasticsearchUuid = parts[1].Trim(); + if (string.IsNullOrWhiteSpace(elasticsearchUuid)) + throw new ArgumentException($"Parameter {nameof(cloudId)} decoded base_64_data contains no elasticsearch UUID, {exceptionSuffix}", nameof(cloudId)); - /// - protected override void Dispose(bool disposing) => base.Dispose(disposing); + return new ParsedCloudId(clusterName, new Uri($"https://{elasticsearchUuid}.{domainName}")); } + + /// + protected override void Dispose(bool disposing) => base.Dispose(disposing); } diff --git a/src/Elastic.Transport/Components/NodePool/Node.cs b/src/Elastic.Transport/Components/NodePool/Node.cs index f943d8b..2940d20 100644 --- a/src/Elastic.Transport/Components/NodePool/Node.cs +++ b/src/Elastic.Transport/Components/NodePool/Node.cs @@ -7,158 +7,157 @@ using System.Linq; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Represents an endpoint with additional associated metadata on which the can act. +/// +public sealed class Node : IEquatable { + private IReadOnlyCollection _features; + + /// + public Node(Uri uri, IEnumerable features = null) + { + // This make sures that a node can be rooted at a path to. Without the trailing slash Uri's will remove `instance` from + // http://my-saas-provider.com/instance + // Where this might be the user specific path + if (!uri.OriginalString.EndsWith("/", StringComparison.Ordinal)) + uri = new Uri(uri.OriginalString + "/"); + Uri = uri; + IsAlive = true; + if (features is IReadOnlyCollection s) + Features = s; + else + Features = features?.ToList().AsReadOnly() ?? EmptyReadOnly.Collection; + IsResurrected = true; + } + + private HashSet _featureSet; + /// - /// Represents an endpoint with additional associated metadata on which the can act. + /// A readonly collection backed by an that signals what features are enabled on the node. + /// This is loosely typed as to be agnostic to what solution the transport ends up talking to /// - public sealed class Node : IEquatable + public IReadOnlyCollection Features { - private IReadOnlyCollection _features; - - /// - public Node(Uri uri, IEnumerable features = null) + get => _features; + set { - // This make sures that a node can be rooted at a path to. Without the trailing slash Uri's will remove `instance` from - // http://my-saas-provider.com/instance - // Where this might be the user specific path - if (!uri.OriginalString.EndsWith("/", StringComparison.Ordinal)) - uri = new Uri(uri.OriginalString + "/"); - Uri = uri; - IsAlive = true; - if (features is IReadOnlyCollection s) - Features = s; - else - Features = features?.ToList().AsReadOnly() ?? EmptyReadOnly.Collection; - IsResurrected = true; + _features = value; + _featureSet = new HashSet(_features); } + } - private HashSet _featureSet; - - /// - /// A readonly collection backed by an that signals what features are enabled on the node. - /// This is loosely typed as to be agnostic to what solution the transport ends up talking to - /// - public IReadOnlyCollection Features - { - get => _features; - set - { - _features = value; - _featureSet = new HashSet(_features); - } - } + /// + /// Settings as returned by the server, can be used in various ways later on. E.g can use it + /// to only select certain nodes with a setting + /// + public IReadOnlyDictionary Settings { get; set; } = EmptyReadOnly.Dictionary; - /// - /// Settings as returned by the server, can be used in various ways later on. E.g can use it - /// to only select certain nodes with a setting - /// - public IReadOnlyDictionary Settings { get; set; } = EmptyReadOnly.Dictionary; + /// The id of the node, defaults to null when unknown/unspecified + public string Id { get; internal set; } - /// The id of the node, defaults to null when unknown/unspecified - public string Id { get; internal set; } + /// The name of the node, defaults to null when unknown/unspecified + public string Name { get; set; } - /// The name of the node, defaults to null when unknown/unspecified - public string Name { get; set; } + /// The base endpoint where the node can be reached + public Uri Uri { get; } - /// The base endpoint where the node can be reached - public Uri Uri { get; } + /// + /// Indicates whether the node is alive. can take nodes out of rotation by calling + /// on . + /// + public bool IsAlive { get; private set; } - /// - /// Indicates whether the node is alive. can take nodes out of rotation by calling - /// on . - /// - public bool IsAlive { get; private set; } + /// When marked dead this reflects the date that the node has to be taken out of rotation till + public DateTime DeadUntil { get; private set; } - /// When marked dead this reflects the date that the node has to be taken out of rotation till - public DateTime DeadUntil { get; private set; } + /// The number of failed attempts trying to use this node, resets when a node is marked alive + public int FailedAttempts { get; private set; } - /// The number of failed attempts trying to use this node, resets when a node is marked alive - public int FailedAttempts { get; private set; } + /// When set this signals the transport that a ping before first usage would be wise + public bool IsResurrected { get; set; } - /// When set this signals the transport that a ping before first usage would be wise - public bool IsResurrected { get; set; } + /// + /// Returns true if the has enabled, or NO features are known on the node. + /// The assumption being if no have been discovered ALL features are enabled + /// + public bool HasFeature(string feature) => _features.Count == 0 || _featureSet.Contains(feature); - /// - /// Returns true if the has enabled, or NO features are known on the node. - /// The assumption being if no have been discovered ALL features are enabled - /// - public bool HasFeature(string feature) => _features.Count == 0 || _featureSet.Contains(feature); + /// + /// Marks this node as dead and set the date (see ) after which we want it to come back alive + /// + /// The after which this node should be considered alive again + public void MarkDead(DateTime untill) + { + FailedAttempts++; + IsAlive = false; + IsResurrected = false; + DeadUntil = untill; + } - /// - /// Marks this node as dead and set the date (see ) after which we want it to come back alive - /// - /// The after which this node should be considered alive again - public void MarkDead(DateTime untill) - { - FailedAttempts++; - IsAlive = false; - IsResurrected = false; - DeadUntil = untill; - } + /// Mark the node alive explicitly + public void MarkAlive() + { + FailedAttempts = 0; + IsAlive = true; + IsResurrected = false; + DeadUntil = default(DateTime); + } - /// Mark the node alive explicitly - public void MarkAlive() - { - FailedAttempts = 0; - IsAlive = true; - IsResurrected = false; - DeadUntil = default(DateTime); - } + /// + /// Use the nodes uri as root to create a with + /// + public Uri CreatePath(string path) => new Uri(Uri, path); - /// - /// Use the nodes uri as root to create a with - /// - public Uri CreatePath(string path) => new Uri(Uri, path); - - /// - /// Create a clone of the current node. This is used by implementations that supports reseeding the - /// list of nodes through - /// - public Node Clone() => - new Node(Uri, Features) - { - IsResurrected = IsResurrected, - Id = Id, - Name = Name, - FailedAttempts = FailedAttempts, - DeadUntil = DeadUntil, - IsAlive = IsAlive, - Settings = Settings, - }; - - /// Two 's that point to the same are considered equal - public bool Equals(Node other) + /// + /// Create a clone of the current node. This is used by implementations that supports reseeding the + /// list of nodes through + /// + public Node Clone() => + new Node(Uri, Features) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + IsResurrected = IsResurrected, + Id = Id, + Name = Name, + FailedAttempts = FailedAttempts, + DeadUntil = DeadUntil, + IsAlive = IsAlive, + Settings = Settings, + }; + + /// Two 's that point to the same are considered equal + public bool Equals(Node other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; - return Uri == other.Uri; - } + return Uri == other.Uri; + } - /// - public static bool operator ==(Node left, Node right) => - // ReSharper disable once MergeConditionalExpression - ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.Equals(right); + /// + public static bool operator ==(Node left, Node right) => + // ReSharper disable once MergeConditionalExpression + ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.Equals(right); - /// - public static bool operator !=(Node left, Node right) => !(left == right); + /// + public static bool operator !=(Node left, Node right) => !(left == right); - /// - public static implicit operator Node(Uri uri) => new Node(uri); + /// + public static implicit operator Node(Uri uri) => new Node(uri); - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - - return Equals((Node)obj); - } + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; - /// A nodes identify is solely based on its - public override int GetHashCode() => Uri.GetHashCode(); + return Equals((Node)obj); } + + /// A nodes identify is solely based on its + public override int GetHashCode() => Uri.GetHashCode(); } diff --git a/src/Elastic.Transport/Components/NodePool/NodePool.cs b/src/Elastic.Transport/Components/NodePool/NodePool.cs index 629a23d..5f6492d 100644 --- a/src/Elastic.Transport/Components/NodePool/NodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/NodePool.cs @@ -6,96 +6,95 @@ using System.Collections.Generic; using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A node pool is responsible for maintaining a read only collection of (s) under . +/// +/// Unlike the name might suggest this component is not responsible for IO level pooling. For that we rely on abstracting away +/// the connection IO pooling. +/// +/// This interface signals the current connection strategy to . +/// +public abstract class NodePool : IDisposable { + private bool _disposed; + /// - /// A node pool is responsible for maintaining a read only collection of (s) under . - /// - /// Unlike the name might suggest this component is not responsible for IO level pooling. For that we rely on abstracting away - /// the connection IO pooling. - /// - /// This interface signals the current connection strategy to . + /// The last time that this instance was updated. /// - public abstract class NodePool : IDisposable - { - private bool _disposed; + public abstract DateTime LastUpdate { get; protected set; } - /// - /// The last time that this instance was updated. - /// - public abstract DateTime LastUpdate { get; protected set; } - - /// - /// Returns the default maximum retries for the connection pool implementation. - /// Most implementations default to number of nodes, note that this can be overridden - /// in the connection settings. - /// - public abstract int MaxRetries { get; } + /// + /// Returns the default maximum retries for the connection pool implementation. + /// Most implementations default to number of nodes, note that this can be overridden + /// in the connection settings. + /// + public abstract int MaxRetries { get; } - /// - /// Returns a read only view of all the nodes in the cluster, which might involve creating copies of nodes e.g - /// if you are using . - /// If you do not need an isolated copy of the nodes, please read to completion. - /// - public abstract IReadOnlyCollection Nodes { get; } + /// + /// Returns a read only view of all the nodes in the cluster, which might involve creating copies of nodes e.g + /// if you are using . + /// If you do not need an isolated copy of the nodes, please read to completion. + /// + public abstract IReadOnlyCollection Nodes { get; } - /// - /// Whether a sniff is seen on startup. - /// - public bool SniffedOnStartup { get; private set; } + /// + /// Whether a sniff is seen on startup. + /// + public bool SniffedOnStartup { get; private set; } - /// - /// Whether a sniff is seen on startup. The implementation is - /// responsible for setting this in a thread safe fashion. - /// - public void MarkAsSniffed() => SniffedOnStartup = true; + /// + /// Whether a sniff is seen on startup. The implementation is + /// responsible for setting this in a thread safe fashion. + /// + public void MarkAsSniffed() => SniffedOnStartup = true; - /// - /// Whether pinging is supported. - /// - public abstract bool SupportsPinging { get; } + /// + /// Whether pinging is supported. + /// + public abstract bool SupportsPinging { get; } - /// - /// Whether reseeding with new nodes is supported. - /// - public abstract bool SupportsReseeding { get; } + /// + /// Whether reseeding with new nodes is supported. + /// + public abstract bool SupportsReseeding { get; } - /// - /// Whether SSL/TLS is being used. - /// - public abstract bool UsingSsl { get; protected set; } + /// + /// Whether SSL/TLS is being used. + /// + public abstract bool UsingSsl { get; protected set; } - /// - /// - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// - /// - /// - protected virtual void Dispose(bool disposing) + /// + /// + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) { - if (!_disposed) - { - _disposed = true; - } + _disposed = true; } + } - /// - /// Creates a view over the nodes, with changing starting positions, that wraps over on each call - /// e.g Thread A might get 1,2,3,4,5 and thread B will get 2,3,4,5,1. - /// if there are no live nodes yields a different dead node to try once - /// - public abstract IEnumerable CreateView(Action audit = null); + /// + /// Creates a view over the nodes, with changing starting positions, that wraps over on each call + /// e.g Thread A might get 1,2,3,4,5 and thread B will get 2,3,4,5,1. + /// if there are no live nodes yields a different dead node to try once + /// + public abstract IEnumerable CreateView(Action audit = null); - /// - /// Reseeds the nodes. The implementation is responsible for thread safety. - /// - public abstract void Reseed(IEnumerable nodes); - } + /// + /// Reseeds the nodes. The implementation is responsible for thread safety. + /// + public abstract void Reseed(IEnumerable nodes); } diff --git a/src/Elastic.Transport/Components/NodePool/SingleNodePool.cs b/src/Elastic.Transport/Components/NodePool/SingleNodePool.cs index 5465077..34bbf19 100644 --- a/src/Elastic.Transport/Components/NodePool/SingleNodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/SingleNodePool.cs @@ -6,45 +6,44 @@ using System.Collections.Generic; using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// A pool to a single node or endpoint. +public class SingleNodePool : NodePool { - /// A pool to a single node or endpoint. - public class SingleNodePool : NodePool + /// + public SingleNodePool(Uri uri, DateTimeProvider dateTimeProvider = null) { - /// - public SingleNodePool(Uri uri, IDateTimeProvider dateTimeProvider = null) - { - var node = new Node(uri); - UsingSsl = node.Uri.Scheme == "https"; - Nodes = new List { node }; - LastUpdate = (dateTimeProvider ?? DateTimeProvider.Default).Now(); - } + var node = new Node(uri); + UsingSsl = node.Uri.Scheme == "https"; + Nodes = new List { node }; + LastUpdate = (dateTimeProvider ?? DefaultDateTimeProvider.Default).Now(); + } - /// - public override DateTime LastUpdate { get; protected set; } + /// + public override DateTime LastUpdate { get; protected set; } - /// - public override int MaxRetries => 0; + /// + public override int MaxRetries => 0; - /// - public override IReadOnlyCollection Nodes { get; } + /// + public override IReadOnlyCollection Nodes { get; } - /// - public override bool SupportsPinging => false; + /// + public override bool SupportsPinging => false; - /// - public override bool SupportsReseeding => false; + /// + public override bool SupportsReseeding => false; - /// - public override bool UsingSsl { get; protected set; } + /// + public override bool UsingSsl { get; protected set; } - /// - public override IEnumerable CreateView(Action audit = null) => Nodes; + /// + public override IEnumerable CreateView(Action audit = null) => Nodes; - /// - public override void Reseed(IEnumerable nodes) { } //ignored + /// + public override void Reseed(IEnumerable nodes) { } //ignored - /// - protected override void Dispose(bool disposing) => base.Dispose(disposing); - } + /// + protected override void Dispose(bool disposing) => base.Dispose(disposing); } diff --git a/src/Elastic.Transport/Components/NodePool/SniffingNodePool.cs b/src/Elastic.Transport/Components/NodePool/SniffingNodePool.cs index 05dd520..3c88c1c 100644 --- a/src/Elastic.Transport/Components/NodePool/SniffingNodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/SniffingNodePool.cs @@ -9,105 +9,104 @@ using Elastic.Transport.Diagnostics.Auditing; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A node pool that enables which in turn allows the to enable sniffing to +/// discover the current cluster's list of active nodes. +/// +public class SniffingNodePool : StaticNodePool { - /// - /// A node pool that enables which in turn allows the to enable sniffing to - /// discover the current cluster's list of active nodes. - /// - public class SniffingNodePool : StaticNodePool - { - private bool _disposed; + private bool _disposed; - private readonly ReaderWriterLockSlim _readerWriter = new(); + private readonly ReaderWriterLockSlim _readerWriter = new(); - /// > - public SniffingNodePool(IEnumerable uris, bool randomize = true, IDateTimeProvider dateTimeProvider = null) - : base(uris, randomize, dateTimeProvider) { } + /// > + public SniffingNodePool(IEnumerable uris, bool randomize = true, DateTimeProvider dateTimeProvider = null) + : base(uris, randomize, dateTimeProvider) { } - /// > - public SniffingNodePool(IEnumerable nodes, bool randomize = true, IDateTimeProvider dateTimeProvider = null) - : base(nodes, randomize, dateTimeProvider) { } + /// > + public SniffingNodePool(IEnumerable nodes, bool randomize = true, DateTimeProvider dateTimeProvider = null) + : base(nodes, randomize, dateTimeProvider) { } - /// > - public SniffingNodePool(IEnumerable nodes, Func nodeScorer, IDateTimeProvider dateTimeProvider = null) - : base(nodes, nodeScorer, dateTimeProvider) { } + /// > + public SniffingNodePool(IEnumerable nodes, Func nodeScorer, DateTimeProvider dateTimeProvider = null) + : base(nodes, nodeScorer, dateTimeProvider) { } - /// - public override IReadOnlyCollection Nodes + /// + public override IReadOnlyCollection Nodes + { + get { - get + try { - try - { - //since internalnodes can be changed after returning we return - //a completely new list of cloned nodes - _readerWriter.EnterReadLock(); - return InternalNodes.Select(n => n.Clone()).ToList(); - } - finally - { - _readerWriter.ExitReadLock(); - } + //since internalnodes can be changed after returning we return + //a completely new list of cloned nodes + _readerWriter.EnterReadLock(); + return InternalNodes.Select(n => n.Clone()).ToList(); + } + finally + { + _readerWriter.ExitReadLock(); } } + } - /// - public override bool SupportsPinging => true; + /// + public override bool SupportsPinging => true; - /// - public override bool SupportsReseeding => true; + /// + public override bool SupportsReseeding => true; - /// - public override void Reseed(IEnumerable nodes) - { - if (!nodes.HasAny(out var nodesArray)) return; + /// + public override void Reseed(IEnumerable nodes) + { + if (!nodes.HasAny(out var nodesArray)) return; - try - { - _readerWriter.EnterWriteLock(); - var sortedNodes = SortNodes(nodesArray) - .DistinctByCustom(n => n.Uri) - .ToList(); - - InternalNodes = sortedNodes; - GlobalCursor = -1; - LastUpdate = DateTimeProvider.Now(); - } - finally - { - _readerWriter.ExitWriteLock(); - } + try + { + _readerWriter.EnterWriteLock(); + var sortedNodes = SortNodes(nodesArray) + .DistinctByCustom(n => n.Uri) + .ToList(); + + InternalNodes = sortedNodes; + GlobalCursor = -1; + LastUpdate = DateTimeProvider.Now(); } + finally + { + _readerWriter.ExitWriteLock(); + } + } - /// - public override IEnumerable CreateView(Action audit = null) + /// + public override IEnumerable CreateView(Action audit = null) + { + _readerWriter.EnterReadLock(); + try { - _readerWriter.EnterReadLock(); - try - { - return base.CreateView(audit); - } - finally - { - _readerWriter.ExitReadLock(); - } + return base.CreateView(audit); + } + finally + { + _readerWriter.ExitReadLock(); } + } - /// - protected override void Dispose(bool disposing) + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) { - if (!_disposed) + if (disposing) { - if (disposing) - { - _readerWriter?.Dispose(); - } - - _disposed = true; + _readerWriter?.Dispose(); } - base.Dispose(disposing); + _disposed = true; } + + base.Dispose(disposing); } } diff --git a/src/Elastic.Transport/Components/NodePool/StaticNodePool.cs b/src/Elastic.Transport/Components/NodePool/StaticNodePool.cs index a779526..a631943 100644 --- a/src/Elastic.Transport/Components/NodePool/StaticNodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/StaticNodePool.cs @@ -9,206 +9,205 @@ using Elastic.Transport.Diagnostics.Auditing; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A node pool that disables which in turn disallows the to enable sniffing to +/// discover the current cluster's list of active nodes. +/// Therefore the nodes you supply are the list of known nodes throughout its lifetime, hence static +/// +public class StaticNodePool : NodePool { /// - /// A node pool that disables which in turn disallows the to enable sniffing to - /// discover the current cluster's list of active nodes. - /// Therefore the nodes you supply are the list of known nodes throughout its lifetime, hence static + /// Everytime is called it picks the initial starting point from this cursor. + /// After which it uses a local cursor to commence the enumeration. This makes deterministic + /// even when if multiple threads enumerate over multiple lazy collections returned by . /// - public class StaticNodePool : NodePool - { - /// - /// Everytime is called it picks the initial starting point from this cursor. - /// After which it uses a local cursor to commence the enumeration. This makes deterministic - /// even when if multiple threads enumerate over multiple lazy collections returned by . - /// - protected int GlobalCursor = -1; + protected int GlobalCursor = -1; - private readonly Func _nodeScorer; + private readonly Func _nodeScorer; - /// - public StaticNodePool(IEnumerable uris, bool randomize = true, IDateTimeProvider dateTimeProvider = null) - : this(uris.Select(uri => new Node(uri)), randomize, null, dateTimeProvider) { } + /// + public StaticNodePool(IEnumerable uris, bool randomize = true, DateTimeProvider dateTimeProvider = null) + : this(uris.Select(uri => new Node(uri)), randomize, null, dateTimeProvider) { } - /// - public StaticNodePool(IEnumerable nodes, bool randomize = true, IDateTimeProvider dateTimeProvider = null) - : this(nodes, randomize, null, dateTimeProvider) { } + /// + public StaticNodePool(IEnumerable nodes, bool randomize = true, DateTimeProvider dateTimeProvider = null) + : this(nodes, randomize, null, dateTimeProvider) { } - /// - protected StaticNodePool(IEnumerable nodes, bool randomize, int? randomizeSeed = null, IDateTimeProvider dateTimeProvider = null) - { - Randomize = randomize; - Random = !randomize || !randomizeSeed.HasValue - ? new Random() - : new Random(randomizeSeed.Value); + /// + protected StaticNodePool(IEnumerable nodes, bool randomize, int? randomizeSeed = null, DateTimeProvider dateTimeProvider = null) + { + Randomize = randomize; + Random = !randomize || !randomizeSeed.HasValue + ? new Random() + : new Random(randomizeSeed.Value); - Initialize(nodes, dateTimeProvider); - } + Initialize(nodes, dateTimeProvider); + } - //this constructor is protected because nodeScorer only makes sense on subclasses that support reseeding otherwise just manually sort `nodes` before instantiating. - /// - protected StaticNodePool(IEnumerable nodes, Func nodeScorer = null, IDateTimeProvider dateTimeProvider = null) - { - _nodeScorer = nodeScorer; - Initialize(nodes, dateTimeProvider); - } + //this constructor is protected because nodeScorer only makes sense on subclasses that support reseeding otherwise just manually sort `nodes` before instantiating. + /// + protected StaticNodePool(IEnumerable nodes, Func nodeScorer = null, DateTimeProvider dateTimeProvider = null) + { + _nodeScorer = nodeScorer; + Initialize(nodes, dateTimeProvider); + } - private void Initialize(IEnumerable nodes, IDateTimeProvider dateTimeProvider) - { - var nodesProvided = nodes?.ToList() ?? throw new ArgumentNullException(nameof(nodes)); - nodesProvided.ThrowIfEmpty(nameof(nodes)); - DateTimeProvider = dateTimeProvider ?? Elastic.Transport.DateTimeProvider.Default; + private void Initialize(IEnumerable nodes, DateTimeProvider dateTimeProvider) + { + var nodesProvided = nodes?.ToList() ?? throw new ArgumentNullException(nameof(nodes)); + nodesProvided.ThrowIfEmpty(nameof(nodes)); + DateTimeProvider = dateTimeProvider ?? Elastic.Transport.DefaultDateTimeProvider.Default; - string scheme = null; - foreach (var node in nodesProvided) + string scheme = null; + foreach (var node in nodesProvided) + { + if (scheme == null) { - if (scheme == null) - { - scheme = node.Uri.Scheme; - UsingSsl = scheme == "https"; - } - else if (scheme != node.Uri.Scheme) - throw new ArgumentException("Trying to instantiate a connection pool with mixed URI Schemes"); + scheme = node.Uri.Scheme; + UsingSsl = scheme == "https"; } - - InternalNodes = SortNodes(nodesProvided) - .DistinctByCustom(n => n.Uri) - .ToList(); - LastUpdate = DateTimeProvider.Now(); + else if (scheme != node.Uri.Scheme) + throw new ArgumentException("Trying to instantiate a connection pool with mixed URI Schemes"); } - /// - public override DateTime LastUpdate { get; protected set; } + InternalNodes = SortNodes(nodesProvided) + .DistinctByCustom(n => n.Uri) + .ToList(); + LastUpdate = DateTimeProvider.Now(); + } - /// - public override int MaxRetries => InternalNodes.Count - 1; + /// + public override DateTime LastUpdate { get; protected set; } - /// - public override IReadOnlyCollection Nodes => InternalNodes; + /// + public override int MaxRetries => InternalNodes.Count - 1; - /// - public override bool SupportsPinging => true; + /// + public override IReadOnlyCollection Nodes => InternalNodes; - /// - public override bool SupportsReseeding => false; + /// + public override bool SupportsPinging => true; - /// - public override bool UsingSsl { get; protected set; } + /// + public override bool SupportsReseeding => false; - /// - /// A window into that only selects the nodes considered alive at the time of calling - /// this property. Taking into account and - /// - protected IReadOnlyList AliveNodes - { - get - { - var now = DateTimeProvider.Now(); - return InternalNodes - .Where(n => n.IsAlive || n.DeadUntil <= now) - .ToList(); - } - } + /// + public override bool UsingSsl { get; protected set; } - /// > - protected IDateTimeProvider DateTimeProvider { get; private set; } - - /// - /// The list of nodes we are operating over. This is protected so that subclasses that DO implement - /// can update this list. Its up to subclasses to make this thread safe. - /// - protected List InternalNodes { get; set; } - - /// - /// If is set sub classes that support reseeding will have to use this instance since it might be based of an - /// explicit seed passed into the constructor. - /// - // ReSharper disable once MemberCanBePrivate.Global - protected Random Random { get; } - - /// Whether the nodes order should be randomized after sniffing - // ReSharper disable once MemberCanBePrivate.Global - protected bool Randomize { get; } - - /// - /// Creates a view of all the live nodes with changing starting positions that wraps over on each call - /// e.g Thread A might get 1,2,3,4,5 and thread B will get 2,3,4,5,1. - /// if there are no live nodes yields a different dead node to try once - /// - public override IEnumerable CreateView(Action audit = null) + /// + /// A window into that only selects the nodes considered alive at the time of calling + /// this property. Taking into account and + /// + protected IReadOnlyList AliveNodes + { + get { - var nodes = AliveNodes; + var now = DateTimeProvider.Now(); + return InternalNodes + .Where(n => n.IsAlive || n.DeadUntil <= now) + .ToList(); + } + } - var globalCursor = Interlocked.Increment(ref GlobalCursor); + /// > + protected DateTimeProvider DateTimeProvider { get; private set; } - if (nodes.Count == 0) - { - //could not find a suitable node retrying on first node off globalCursor - yield return RetryInternalNodes(globalCursor, audit); + /// + /// The list of nodes we are operating over. This is protected so that subclasses that DO implement + /// can update this list. Its up to subclasses to make this thread safe. + /// + protected List InternalNodes { get; set; } - yield break; - } + /// + /// If is set sub classes that support reseeding will have to use this instance since it might be based of an + /// explicit seed passed into the constructor. + /// + // ReSharper disable once MemberCanBePrivate.Global + protected Random Random { get; } - var localCursor = globalCursor % nodes.Count; - foreach (var aliveNode in SelectAliveNodes(localCursor, nodes, audit)) yield return aliveNode; - } + /// Whether the nodes order should be randomized after sniffing + // ReSharper disable once MemberCanBePrivate.Global + protected bool Randomize { get; } - /// - public override void Reseed(IEnumerable nodes) { } //ignored + /// + /// Creates a view of all the live nodes with changing starting positions that wraps over on each call + /// e.g Thread A might get 1,2,3,4,5 and thread B will get 2,3,4,5,1. + /// if there are no live nodes yields a different dead node to try once + /// + public override IEnumerable CreateView(Action audit = null) + { + var nodes = AliveNodes; + var globalCursor = Interlocked.Increment(ref GlobalCursor); - /// - /// If no active nodes are found this method can be used by subclasses to reactivate the next node based on - /// - /// - /// - /// Trace action to document the fact all nodes were dead and were resurrecting one at random - protected Node RetryInternalNodes(int globalCursor, Action audit = null) + if (nodes.Count == 0) { - audit?.Invoke(AuditEvent.AllNodesDead, null); - var node = InternalNodes[globalCursor % InternalNodes.Count]; - node.IsResurrected = true; - audit?.Invoke(AuditEvent.Resurrection, node); + //could not find a suitable node retrying on first node off globalCursor + yield return RetryInternalNodes(globalCursor, audit); - return node; + yield break; } - /// - /// Lazy enumerate based on the local . Enumeration will start from - /// and loop around the end and stop before hitting again. This ensures all nodes are attempted. - /// - /// The starting point into from wich to start. - /// - /// Trace action to notify if a resurrection occured - protected static IEnumerable SelectAliveNodes(int cursor, IReadOnlyList aliveNodes, Action audit = null) + var localCursor = globalCursor % nodes.Count; + foreach (var aliveNode in SelectAliveNodes(localCursor, nodes, audit)) yield return aliveNode; + } + + /// + public override void Reseed(IEnumerable nodes) { } //ignored + + + /// + /// If no active nodes are found this method can be used by subclasses to reactivate the next node based on + /// + /// + /// + /// Trace action to document the fact all nodes were dead and were resurrecting one at random + protected Node RetryInternalNodes(int globalCursor, Action audit = null) + { + audit?.Invoke(AuditEvent.AllNodesDead, null); + var node = InternalNodes[globalCursor % InternalNodes.Count]; + node.IsResurrected = true; + audit?.Invoke(AuditEvent.Resurrection, node); + + return node; + } + + /// + /// Lazy enumerate based on the local . Enumeration will start from + /// and loop around the end and stop before hitting again. This ensures all nodes are attempted. + /// + /// The starting point into from wich to start. + /// + /// Trace action to notify if a resurrection occured + protected static IEnumerable SelectAliveNodes(int cursor, IReadOnlyList aliveNodes, Action audit = null) + { + // ReSharper disable once ForCanBeConvertedToForeach + for (var attempts = 0; attempts < aliveNodes.Count; attempts++) { - // ReSharper disable once ForCanBeConvertedToForeach - for (var attempts = 0; attempts < aliveNodes.Count; attempts++) + var node = aliveNodes[cursor]; + cursor = (cursor + 1) % aliveNodes.Count; + //if this node is not alive or no longer dead mark it as resurrected + if (!node.IsAlive) { - var node = aliveNodes[cursor]; - cursor = (cursor + 1) % aliveNodes.Count; - //if this node is not alive or no longer dead mark it as resurrected - if (!node.IsAlive) - { - audit?.Invoke(AuditEvent.Resurrection, node); - node.IsResurrected = true; - } - - yield return node; + audit?.Invoke(AuditEvent.Resurrection, node); + node.IsResurrected = true; } - } - /// - /// Provides the default sort order for this takes into account whether a subclass injected a custom comparer - /// and if not whether is set - /// - protected IOrderedEnumerable SortNodes(IEnumerable nodes) => - _nodeScorer != null - ? nodes.OrderByDescending(_nodeScorer) - : nodes.OrderBy(n => Randomize ? Random.Next() : 1); - - /// - protected override void Dispose(bool disposing) => base.Dispose(disposing); + yield return node; + } } + + /// + /// Provides the default sort order for this takes into account whether a subclass injected a custom comparer + /// and if not whether is set + /// + protected IOrderedEnumerable SortNodes(IEnumerable nodes) => + _nodeScorer != null + ? nodes.OrderByDescending(_nodeScorer) + : nodes.OrderBy(n => Randomize ? Random.Next() : 1); + + /// + protected override void Dispose(bool disposing) => base.Dispose(disposing); } diff --git a/src/Elastic.Transport/Components/NodePool/StickyNodePool.cs b/src/Elastic.Transport/Components/NodePool/StickyNodePool.cs index 1242c00..fab319e 100644 --- a/src/Elastic.Transport/Components/NodePool/StickyNodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/StickyNodePool.cs @@ -7,48 +7,47 @@ using System.Threading; using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport -{ - /// - /// A connection pool implementation that does not support reseeding and stays on the first reporting true for . - /// This is great if for instance you have multiple proxies that you can fallback on allowing you to seed the proxies in order of preference. - /// - public sealed class StickyNodePool : StaticNodePool - { - /// - public StickyNodePool(IEnumerable uris, IDateTimeProvider dateTimeProvider = null) - : base(uris, false, dateTimeProvider) { } - - /// - public StickyNodePool(IEnumerable nodes, IDateTimeProvider dateTimeProvider = null) - : base(nodes, false, dateTimeProvider) { } +namespace Elastic.Transport; - /// - public override IEnumerable CreateView(Action audit = null) - { - var nodes = AliveNodes; +/// +/// A connection pool implementation that does not support reseeding and stays on the first reporting true for . +/// This is great if for instance you have multiple proxies that you can fallback on allowing you to seed the proxies in order of preference. +/// +public sealed class StickyNodePool : StaticNodePool +{ + /// + public StickyNodePool(IEnumerable uris, DateTimeProvider dateTimeProvider = null) + : base(uris, false, dateTimeProvider) { } - if (nodes.Count == 0) - { - var globalCursor = Interlocked.Increment(ref GlobalCursor); + /// + public StickyNodePool(IEnumerable nodes, DateTimeProvider dateTimeProvider = null) + : base(nodes, false, dateTimeProvider) { } - //could not find a suitable node retrying on first node off globalCursor - yield return RetryInternalNodes(globalCursor, audit); + /// + public override IEnumerable CreateView(Action audit = null) + { + var nodes = AliveNodes; - yield break; - } + if (nodes.Count == 0) + { + var globalCursor = Interlocked.Increment(ref GlobalCursor); - // If the cursor is greater than the default then it's been - // set already but we now have a live node so we should reset it - if (GlobalCursor > -1) - Interlocked.Exchange(ref GlobalCursor, -1); + //could not find a suitable node retrying on first node off globalCursor + yield return RetryInternalNodes(globalCursor, audit); - var localCursor = 0; - foreach (var aliveNode in SelectAliveNodes(localCursor, nodes, audit)) - yield return aliveNode; + yield break; } - /// - public override void Reseed(IEnumerable nodes) { } + // If the cursor is greater than the default then it's been + // set already but we now have a live node so we should reset it + if (GlobalCursor > -1) + Interlocked.Exchange(ref GlobalCursor, -1); + + var localCursor = 0; + foreach (var aliveNode in SelectAliveNodes(localCursor, nodes, audit)) + yield return aliveNode; } + + /// + public override void Reseed(IEnumerable nodes) { } } diff --git a/src/Elastic.Transport/Components/NodePool/StickySniffingNodePool.cs b/src/Elastic.Transport/Components/NodePool/StickySniffingNodePool.cs index baca685..e4344f4 100644 --- a/src/Elastic.Transport/Components/NodePool/StickySniffingNodePool.cs +++ b/src/Elastic.Transport/Components/NodePool/StickySniffingNodePool.cs @@ -8,54 +8,53 @@ using System.Threading; using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport -{ - /// - /// A connection pool implementation that supports reseeding but stays on the first reporting true for . - /// This is great if for instance you have multiple proxies that you can fallback on allowing you to seed the proxies in order of preference. - /// - public sealed class StickySniffingNodePool : SniffingNodePool - { - /// - public StickySniffingNodePool(IEnumerable uris, Func nodeScorer, IDateTimeProvider dateTimeProvider = null) - : base(uris.Select(uri => new Node(uri)), nodeScorer ?? DefaultNodeScore, dateTimeProvider) { } - - /// - public StickySniffingNodePool(IEnumerable nodes, Func nodeScorer, IDateTimeProvider dateTimeProvider = null) - : base(nodes, nodeScorer ?? DefaultNodeScore, dateTimeProvider) { } +namespace Elastic.Transport; - /// - public override bool SupportsPinging => true; +/// +/// A connection pool implementation that supports reseeding but stays on the first reporting true for . +/// This is great if for instance you have multiple proxies that you can fallback on allowing you to seed the proxies in order of preference. +/// +public sealed class StickySniffingNodePool : SniffingNodePool +{ + /// + public StickySniffingNodePool(IEnumerable uris, Func nodeScorer, DateTimeProvider dateTimeProvider = null) + : base(uris.Select(uri => new Node(uri)), nodeScorer ?? DefaultNodeScore, dateTimeProvider) { } - /// - public override bool SupportsReseeding => true; + /// + public StickySniffingNodePool(IEnumerable nodes, Func nodeScorer, DateTimeProvider dateTimeProvider = null) + : base(nodes, nodeScorer ?? DefaultNodeScore, dateTimeProvider) { } - /// - public override IEnumerable CreateView(Action audit = null) - { - var nodes = AliveNodes; + /// + public override bool SupportsPinging => true; - if (nodes.Count == 0) - { - var globalCursor = Interlocked.Increment(ref GlobalCursor); + /// + public override bool SupportsReseeding => true; - //could not find a suitable node retrying on first node off globalCursor - yield return RetryInternalNodes(globalCursor, audit); + /// + public override IEnumerable CreateView(Action audit = null) + { + var nodes = AliveNodes; - yield break; - } + if (nodes.Count == 0) + { + var globalCursor = Interlocked.Increment(ref GlobalCursor); - // If the cursor is greater than the default then it's been - // set already but we now have a live node so we should reset it - if (GlobalCursor > -1) - Interlocked.Exchange(ref GlobalCursor, -1); + //could not find a suitable node retrying on first node off globalCursor + yield return RetryInternalNodes(globalCursor, audit); - var localCursor = 0; - foreach (var aliveNode in SelectAliveNodes(localCursor, nodes, audit)) - yield return aliveNode; + yield break; } - /// Allows subclasses to hook into the parents dispose - private static float DefaultNodeScore(Node node) => 0f; + // If the cursor is greater than the default then it's been + // set already but we now have a live node so we should reset it + if (GlobalCursor > -1) + Interlocked.Exchange(ref GlobalCursor, -1); + + var localCursor = 0; + foreach (var aliveNode in SelectAliveNodes(localCursor, nodes, audit)) + yield return aliveNode; } + + /// Allows subclasses to hook into the parents dispose + private static float DefaultNodeScore(Node node) => 0f; } diff --git a/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs b/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs new file mode 100644 index 0000000..ef053f3 --- /dev/null +++ b/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs @@ -0,0 +1,612 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Transport.Diagnostics; +using Elastic.Transport.Diagnostics.Auditing; +using Elastic.Transport.Extensions; +using Elastic.Transport.Products; +using static Elastic.Transport.Diagnostics.Auditing.AuditEvent; + +//#if NETSTANDARD2_0 || NETSTANDARD2_1 +//using System.Threading.Tasks.Extensions; +//#endif + +namespace Elastic.Transport; + +/// +public class DefaultRequestPipeline : RequestPipeline + where TConfiguration : class, ITransportConfiguration +{ + private readonly TransportClient _transportClient; + private readonly NodePool _nodePool; + private readonly DateTimeProvider _dateTimeProvider; + private readonly MemoryStreamFactory _memoryStreamFactory; + private readonly Func _nodePredicate; + private readonly ProductRegistration _productRegistration; + private readonly TConfiguration _settings; + private readonly ResponseBuilder _responseBuilder; + + private RequestConfiguration _pingAndSniffRequestConfiguration; + + /// + internal DefaultRequestPipeline( + TConfiguration configurationValues, + DateTimeProvider dateTimeProvider, + MemoryStreamFactory memoryStreamFactory, + RequestParameters requestParameters + ) + { + _settings = configurationValues; + _nodePool = _settings.NodePool; + _transportClient = _settings.Connection; + _dateTimeProvider = dateTimeProvider; + _memoryStreamFactory = memoryStreamFactory; + _productRegistration = configurationValues.ProductRegistration; + _responseBuilder = _productRegistration.ResponseBuilder; + _nodePredicate = _settings.NodePredicate ?? _productRegistration.NodePredicate; + RequestConfiguration = requestParameters?.RequestConfiguration; + StartedOn = dateTimeProvider.Now(); + } + + /// + public override List AuditTrail { get; } = new(); + + private RequestConfiguration PingAndSniffRequestConfiguration + { + // Lazily loaded when first required, since not all node pools and configurations support pinging and sniffing. + // This avoids allocating 192B per request for those which do not need to ping or sniff. + get + { + if (_pingAndSniffRequestConfiguration is not null) return _pingAndSniffRequestConfiguration; + + _pingAndSniffRequestConfiguration = new RequestConfiguration + { + PingTimeout = PingTimeout, + RequestTimeout = PingTimeout, + AuthenticationHeader = _settings.Authentication, + EnableHttpPipelining = RequestConfiguration?.EnableHttpPipelining ?? _settings.HttpPipeliningEnabled, + ForceNode = RequestConfiguration?.ForceNode + }; + + return _pingAndSniffRequestConfiguration; + } + } + + //TODO xmldocs +#pragma warning disable 1591 + public bool DepletedRetries => Retried >= MaxRetries + 1 || IsTakingTooLong; + + public override bool FirstPoolUsageNeedsSniffing => + !RequestDisabledSniff + && _nodePool.SupportsReseeding && _settings.SniffsOnStartup && !_nodePool.SniffedOnStartup; + + public override bool IsTakingTooLong + { + get + { + var timeout = _settings.MaxRetryTimeout.GetValueOrDefault(RequestTimeout); + var now = _dateTimeProvider.Now(); + + //we apply a soft margin so that if a request timesout at 59 seconds when the maximum is 60 we also abort. + var margin = timeout.TotalMilliseconds / 100.0 * 98; + var marginTimeSpan = TimeSpan.FromMilliseconds(margin); + var timespanCall = now - StartedOn; + var tookToLong = timespanCall >= marginTimeSpan; + return tookToLong; + } + } + + public override int MaxRetries => + RequestConfiguration?.ForceNode != null + ? 0 + : Math.Min(RequestConfiguration?.MaxRetries ?? _settings.MaxRetries.GetValueOrDefault(int.MaxValue), _nodePool.MaxRetries); + + public bool Refresh { get; private set; } + + public int Retried { get; private set; } + + public IEnumerable SniffNodes => _nodePool + .CreateView(LazyAuditable) + .ToList() + .OrderBy(n => _productRegistration.SniffOrder(n)); + + public override bool SniffsOnConnectionFailure => + !RequestDisabledSniff + && _nodePool.SupportsReseeding && _settings.SniffsOnConnectionFault; + + public override bool SniffsOnStaleCluster => + !RequestDisabledSniff + && _nodePool.SupportsReseeding && _settings.SniffInformationLifeSpan.HasValue; + + public override bool StaleClusterState + { + get + { + if (!SniffsOnStaleCluster) return false; + + // ReSharper disable once PossibleInvalidOperationException + // already checked by SniffsOnStaleCluster + var sniffLifeSpan = _settings.SniffInformationLifeSpan.Value; + + var now = _dateTimeProvider.Now(); + var lastSniff = _nodePool.LastUpdate; + + return sniffLifeSpan < now - lastSniff; + } + } + + public override DateTime StartedOn { get; } + + private TimeSpan PingTimeout => + RequestConfiguration?.PingTimeout + ?? _settings.PingTimeout + ?? (_nodePool.UsingSsl ? TransportConfiguration.DefaultPingTimeoutOnSsl : TransportConfiguration.DefaultPingTimeout); + + private IRequestConfiguration RequestConfiguration { get; } + + private bool RequestDisabledSniff => RequestConfiguration != null && (RequestConfiguration.DisableSniff ?? false); + + private TimeSpan RequestTimeout => RequestConfiguration?.RequestTimeout ?? _settings.RequestTimeout; + + public override void AuditCancellationRequested() => Audit(CancellationRequested).Dispose(); + + public override void BadResponse(ref TResponse response, ApiCallDetails callDetails, RequestData data, TransportException exception) + { + if (response == null) + { + //make sure we copy over the error body in case we disabled direct streaming. + var s = callDetails?.ResponseBodyInBytes == null ? Stream.Null : _memoryStreamFactory.Create(callDetails.ResponseBodyInBytes); + var m = callDetails?.ResponseMimeType ?? RequestData.MimeType; + response = _responseBuilder.ToResponse(data, exception, callDetails?.HttpStatusCode, null, s, m, callDetails?.ResponseBodyInBytes?.Length ?? -1, null, null); + } + + if (response.ApiCallDetails is ApiCallDetails apiCallDetails) + apiCallDetails.AuditTrail = AuditTrail; + } + + public override TResponse CallProductEndpoint(RequestData requestData) + { + using (var audit = Audit(HealthyResponse, requestData.Node)) + using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose( + DiagnosticSources.RequestPipeline.CallProductEndpoint, requestData)) + { + audit.Path = requestData.PathAndQuery; + try + { + var response = _transportClient.Request(requestData); + d.EndState = response.ApiCallDetails; + + if (response.ApiCallDetails is ApiCallDetails apiCallDetails) + apiCallDetails.AuditTrail = AuditTrail; + + ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails, response); + if (!response.ApiCallDetails.Success) audit.Event = requestData.OnFailureAuditEvent; + return response; + } + catch (Exception e) + { + audit.Event = requestData.OnFailureAuditEvent; + audit.Exception = e; + throw; + } + } + } + + public override async Task CallProductEndpointAsync(RequestData requestData, CancellationToken cancellationToken) + { + using (var audit = Audit(HealthyResponse, requestData.Node)) + using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose( + DiagnosticSources.RequestPipeline.CallProductEndpoint, requestData)) + { + audit.Path = requestData.PathAndQuery; + try + { + var response = await _transportClient.RequestAsync(requestData, cancellationToken).ConfigureAwait(false); + d.EndState = response.ApiCallDetails; + + if (response.ApiCallDetails is ApiCallDetails apiCallDetails) + apiCallDetails.AuditTrail = AuditTrail; + + ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails, response); + if (!response.ApiCallDetails.Success) audit.Event = requestData.OnFailureAuditEvent; + return response; + } + catch (Exception e) + { + audit.Event = requestData.OnFailureAuditEvent; + audit.Exception = e; + throw; + } + } + } + + public override TransportException CreateClientException( + TResponse response, ApiCallDetails callDetails, RequestData data, List pipelineExceptions) + { + if (callDetails?.Success ?? false) return null; + + var pipelineFailure = data.OnFailurePipelineFailure; + var innerException = callDetails?.OriginalException; + if (pipelineExceptions.HasAny(out var exs)) + { + pipelineFailure = exs.Last().FailureReason; + innerException = exs.AsAggregateOrFirst(); + } + + var statusCode = callDetails?.HttpStatusCode != null ? callDetails.HttpStatusCode.Value.ToString() : "unknown"; + var resource = callDetails == null + ? "unknown resource" + : $"Status code {statusCode} from: {callDetails.HttpMethod} {callDetails.Uri.PathAndQuery}"; + + var exceptionMessage = innerException?.Message ?? "Request failed to execute"; + + if (IsTakingTooLong) + { + pipelineFailure = PipelineFailure.MaxTimeoutReached; + Audit(MaxTimeoutReached); + exceptionMessage = "Maximum timeout reached while retrying request"; + } + else if (Retried >= MaxRetries && MaxRetries > 0) + { + pipelineFailure = PipelineFailure.MaxRetriesReached; + Audit(MaxRetriesReached); + exceptionMessage = "Maximum number of retries reached"; + + var now = _dateTimeProvider.Now(); + var activeNodes = _nodePool.Nodes.Count(n => n.IsAlive || n.DeadUntil <= now); + if (Retried >= activeNodes) + { + Audit(FailedOverAllNodes); + exceptionMessage += ", failed over to all the known alive nodes before failing"; + } + } + + exceptionMessage += !exceptionMessage.EndsWith(".", StringComparison.Ordinal) ? $". Call: {resource}" : $" Call: {resource}"; + if (response != null && _productRegistration.TryGetServerErrorReason(response, out var reason)) + exceptionMessage += $". ServerError: {reason}"; + + var clientException = new TransportException(pipelineFailure, exceptionMessage, innerException) + { + Request = data, ApiCallDetails = callDetails, AuditTrail = AuditTrail + }; + + return clientException; + } + + public override void FirstPoolUsage(SemaphoreSlim semaphore) + { + if (!FirstPoolUsageNeedsSniffing) return; + + if (!semaphore.Wait(_settings.RequestTimeout)) + { + if (FirstPoolUsageNeedsSniffing) + throw new PipelineException(PipelineFailure.CouldNotStartSniffOnStartup, null); + + return; + } + + if (!FirstPoolUsageNeedsSniffing) + { + semaphore.Release(); + return; + } + + try + { + using (Audit(SniffOnStartup)) + { + Sniff(); + _nodePool.MarkAsSniffed(); + } + } + finally + { + semaphore.Release(); + } + } + + public override async Task FirstPoolUsageAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken) + { + if (!FirstPoolUsageNeedsSniffing) return; + + // TODO cancellationToken could throw here and will bubble out as OperationCancelledException + // everywhere else it would bubble out wrapped in a `UnexpectedTransportException` + var success = await semaphore.WaitAsync(_settings.RequestTimeout, cancellationToken).ConfigureAwait(false); + if (!success) + { + if (FirstPoolUsageNeedsSniffing) + throw new PipelineException(PipelineFailure.CouldNotStartSniffOnStartup, null); + + return; + } + + if (!FirstPoolUsageNeedsSniffing) + { + semaphore.Release(); + return; + } + try + { + using (Audit(SniffOnStartup)) + { + await SniffAsync(cancellationToken).ConfigureAwait(false); + _nodePool.MarkAsSniffed(); + } + } + finally + { + semaphore.Release(); + } + } + + public override void MarkAlive(Node node) => node.MarkAlive(); + + public override void MarkDead(Node node) + { + var deadUntil = _dateTimeProvider.DeadTime(node.FailedAttempts, _settings.DeadTimeout, _settings.MaxDeadTimeout); + node.MarkDead(deadUntil); + Retried++; + } + + /// + public override bool TryGetSingleNode(out Node node) + { + if (_nodePool.Nodes.Count <= 1 && _nodePool.MaxRetries <= _nodePool.Nodes.Count && + !_nodePool.SupportsPinging && !_nodePool.SupportsReseeding) + { + node = _nodePool.Nodes.FirstOrDefault(); + + if (node is not null && _nodePredicate(node)) return true; + } + + node = null; + return false; + } + + public override IEnumerable NextNode() + { + if (RequestConfiguration?.ForceNode != null) + { + yield return new Node(RequestConfiguration.ForceNode); + + yield break; + } + + //This for loop allows to break out of the view state machine if we need to + //force a refresh (after reseeding node pool). We have a hardcoded limit of only + //allowing 100 of these refreshes per call + var refreshed = false; + for (var i = 0; i < 100; i++) + { + if (DepletedRetries) yield break; + + foreach (var node in _nodePool.CreateView(LazyAuditable)) + { + if (DepletedRetries) break; + + if (!_nodePredicate(node)) continue; + + yield return node; + + if (!Refresh) continue; + + Refresh = false; + refreshed = true; + break; + } + //unless a refresh was requested we will not iterate over more then a single view. + //keep in mind refreshes are also still bound to overall maxretry count/timeout. + if (!refreshed) break; + } + } + + public override void Ping(Node node) + { + if (!_productRegistration.SupportsPing) return; + if (PingDisabled(node)) return; + + var pingData = _productRegistration.CreatePingRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); + using (var audit = Audit(PingSuccess, node)) + using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Ping, + pingData)) + { + audit.Path = pingData.PathAndQuery; + try + { + var response = _productRegistration.Ping(_transportClient, pingData); + d.EndState = response.ApiCallDetails; + ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails); + //ping should not silently accept bad but valid http responses + if (!response.ApiCallDetails.Success) + throw new PipelineException(pingData.OnFailurePipelineFailure, response.ApiCallDetails.OriginalException) { Response = response }; + } + catch (Exception e) + { + var response = (e as PipelineException)?.Response; + audit.Event = PingFailure; + audit.Exception = e; + throw new PipelineException(PipelineFailure.PingFailure, e) { Response = response }; + } + } + } + + public override async Task PingAsync(Node node, CancellationToken cancellationToken) + { + if (!_productRegistration.SupportsPing) return; + if (PingDisabled(node)) return; + + var pingData = _productRegistration.CreatePingRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); + using (var audit = Audit(PingSuccess, node)) + using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Ping, + pingData)) + { + audit.Path = pingData.PathAndQuery; + try + { + var response = await _productRegistration.PingAsync(_transportClient, pingData, cancellationToken).ConfigureAwait(false); + d.EndState = response.ApiCallDetails; + ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails); + //ping should not silently accept bad but valid http responses + if (!response.ApiCallDetails.Success) + throw new PipelineException(pingData.OnFailurePipelineFailure, response.ApiCallDetails.OriginalException) { Response = response }; + } + catch (Exception e) + { + var response = (e as PipelineException)?.Response; + audit.Event = PingFailure; + audit.Exception = e; + throw new PipelineException(PipelineFailure.PingFailure, e) { Response = response }; + } + } + } + + public override void Sniff() + { + if (!_productRegistration.SupportsSniff) return; + + var exceptions = new List(); + foreach (var node in SniffNodes) + { + var requestData = + _productRegistration.CreateSniffRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); + using (var audit = Audit(SniffSuccess, node)) + using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Sniff, + requestData)) + using (RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Sniff, requestData)) + try + { + audit.Path = requestData.PathAndQuery; + var (response, nodes) = _productRegistration.Sniff(_transportClient, _nodePool.UsingSsl, requestData); + d.EndState = response.ApiCallDetails; + + ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails); + //sniff should not silently accept bad but valid http responses + if (!response.ApiCallDetails.Success) + throw new PipelineException(requestData.OnFailurePipelineFailure, response.ApiCallDetails.OriginalException) { Response = response }; + + _nodePool.Reseed(nodes); + Refresh = true; + return; + } + catch (Exception e) + { + audit.Event = SniffFailure; + audit.Exception = e; + exceptions.Add(e); + } + } + + throw new PipelineException(PipelineFailure.SniffFailure, exceptions.AsAggregateOrFirst()); + } + + public override async Task SniffAsync(CancellationToken cancellationToken) + { + if (!_productRegistration.SupportsSniff) return; + + var exceptions = new List(); + foreach (var node in SniffNodes) + { + var requestData = + _productRegistration.CreateSniffRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); + using (var audit = Audit(SniffSuccess, node)) + using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Sniff, + requestData)) + try + { + audit.Path = requestData.PathAndQuery; + var (response, nodes) = await _productRegistration + .SniffAsync(_transportClient, _nodePool.UsingSsl, requestData, cancellationToken) + .ConfigureAwait(false); + d.EndState = response.ApiCallDetails; + + ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails); + //sniff should not silently accept bad but valid http responses + if (!response.ApiCallDetails.Success) + throw new PipelineException(requestData.OnFailurePipelineFailure, response.ApiCallDetails.OriginalException) { Response = response }; + + _nodePool.Reseed(nodes); + Refresh = true; + return; + } + catch (Exception e) + { + audit.Event = SniffFailure; + audit.Exception = e; + exceptions.Add(e); + } + } + + throw new PipelineException(PipelineFailure.SniffFailure, exceptions.AsAggregateOrFirst()); + } + + public override void SniffOnConnectionFailure() + { + if (!SniffsOnConnectionFailure) return; + + using (Audit(SniffOnFail)) + Sniff(); + } + + public override async Task SniffOnConnectionFailureAsync(CancellationToken cancellationToken) + { + if (!SniffsOnConnectionFailure) return; + + using (Audit(SniffOnFail)) + await SniffAsync(cancellationToken).ConfigureAwait(false); + } + + public override void SniffOnStaleCluster() + { + if (!StaleClusterState) return; + + using (Audit(AuditEvent.SniffOnStaleCluster)) + { + Sniff(); + _nodePool.MarkAsSniffed(); + } + } + + public override async Task SniffOnStaleClusterAsync(CancellationToken cancellationToken) + { + if (!StaleClusterState) return; + + using (Audit(AuditEvent.SniffOnStaleCluster)) + { + await SniffAsync(cancellationToken).ConfigureAwait(false); + _nodePool.MarkAsSniffed(); + } + } + + public override void ThrowNoNodesAttempted(RequestData requestData, List seenExceptions) + { + var clientException = new TransportException(PipelineFailure.NoNodesAttempted, RequestPipelineStatics.NoNodesAttemptedMessage, + (Exception)null); + using (Audit(NoNodesAttempted)) + throw new UnexpectedTransportException(clientException, seenExceptions) { Request = requestData, AuditTrail = AuditTrail }; + } + + private bool PingDisabled(Node node) => + (RequestConfiguration?.DisablePing).GetValueOrDefault(false) + || _settings.DisablePings || !_nodePool.SupportsPinging || !node.IsResurrected; + + private Auditable Audit(AuditEvent type, Node node = null) => new(type, AuditTrail, _dateTimeProvider, node); + + private static void ThrowBadAuthPipelineExceptionWhenNeeded(ApiCallDetails details, TransportResponse response = null) + { + if (details?.HttpStatusCode == 401) + throw new PipelineException(PipelineFailure.BadAuthentication, details.OriginalException) { Response = response }; + } + + private void LazyAuditable(AuditEvent e, Node n) + { + using (new Auditable(e, AuditTrail, _dateTimeProvider, n)) { } + } +} +#pragma warning restore 1591 diff --git a/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs b/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs index 3650564..e097a66 100644 --- a/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs +++ b/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs @@ -14,338 +14,337 @@ using Elastic.Transport.Diagnostics; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A helper class that deals with handling how a is transformed to the requested +/// implementation. This includes handling optionally buffering based on +/// . And handling short circuiting special responses +/// such as , and +/// +internal class DefaultResponseBuilder : ResponseBuilder where TError : ErrorResponse, new() { + private const int BufferSize = 81920; + + private readonly bool _isEmptyError; + + public DefaultResponseBuilder() => _isEmptyError = typeof(TError) == typeof(EmptyError); + + private static readonly Type[] SpecialTypes = + { + typeof(StringResponse), typeof(BytesResponse), typeof(VoidResponse), typeof(DynamicResponse) + }; + /// - /// A helper class that deals with handling how a is transformed to the requested - /// implementation. This includes handling optionally buffering based on - /// . And handling short circuiting special responses - /// such as , and + /// Create an instance of from /// - internal class DefaultResponseBuilder : ResponseBuilder where TError : ErrorResponse, new() + public override TResponse ToResponse( + RequestData requestData, + Exception ex, + int? statusCode, + Dictionary> headers, + Stream responseStream, + string mimeType, + long contentLength, + IReadOnlyDictionary threadPoolStats, + IReadOnlyDictionary tcpStats + ) { - private const int BufferSize = 81920; + responseStream.ThrowIfNull(nameof(responseStream)); - private readonly bool _isEmptyError; + var details = Initialize(requestData, ex, statusCode, headers, mimeType, threadPoolStats, tcpStats); - public DefaultResponseBuilder() => _isEmptyError = typeof(TError) == typeof(EmptyError); + TResponse response = null; - private static readonly Type[] SpecialTypes = - { - typeof(StringResponse), typeof(BytesResponse), typeof(VoidResponse), typeof(DynamicResponse) - }; + // Only attempt to set the body if the response may have content + if (MayHaveBody(statusCode, requestData.Method, contentLength)) + response = SetBody(details, requestData, responseStream, mimeType); + else + responseStream.Dispose(); - /// - /// Create an instance of from - /// - public override TResponse ToResponse( - RequestData requestData, - Exception ex, - int? statusCode, - Dictionary> headers, - Stream responseStream, - string mimeType, - long contentLength, - IReadOnlyDictionary threadPoolStats, - IReadOnlyDictionary tcpStats - ) - { - responseStream.ThrowIfNull(nameof(responseStream)); + response ??= new TResponse(); - var details = Initialize(requestData, ex, statusCode, headers, mimeType, threadPoolStats, tcpStats); + response.ApiCallDetails = details; + return response; + } - TResponse response = null; + /// + /// Create an instance of from + /// + public override async Task ToResponseAsync( + RequestData requestData, + Exception ex, + int? statusCode, + Dictionary> headers, + Stream responseStream, + string mimeType, + long contentLength, + IReadOnlyDictionary threadPoolStats, + IReadOnlyDictionary tcpStats, + CancellationToken cancellationToken = default + ) + { + responseStream.ThrowIfNull(nameof(responseStream)); - // Only attempt to set the body if the response may have content - if (MayHaveBody(statusCode, requestData.Method, contentLength)) - response = SetBody(details, requestData, responseStream, mimeType); - else - responseStream.Dispose(); + var details = Initialize(requestData, ex, statusCode, headers, mimeType, threadPoolStats, tcpStats); - response ??= new TResponse(); + TResponse response = null; - response.ApiCall = details; - return response; - } + // Only attempt to set the body if the response may have content + if (MayHaveBody(statusCode, requestData.Method, contentLength)) + response = await SetBodyAsync(details, requestData, responseStream, mimeType, + cancellationToken).ConfigureAwait(false); + else + responseStream.Dispose(); - /// - /// Create an instance of from - /// - public override async Task ToResponseAsync( - RequestData requestData, - Exception ex, - int? statusCode, - Dictionary> headers, - Stream responseStream, - string mimeType, - long contentLength, - IReadOnlyDictionary threadPoolStats, - IReadOnlyDictionary tcpStats, - CancellationToken cancellationToken = default - ) - { - responseStream.ThrowIfNull(nameof(responseStream)); + response ??= new TResponse(); - var details = Initialize(requestData, ex, statusCode, headers, mimeType, threadPoolStats, tcpStats); + response.ApiCallDetails = details; + return response; + } - TResponse response = null; + /// + /// A helper which returns true if the response could potentially have a body. + /// + private static bool MayHaveBody(int? statusCode, HttpMethod httpMethod, long contentLength) => + contentLength != 0 && (!statusCode.HasValue || statusCode.Value != 204 && httpMethod != HttpMethod.HEAD); - // Only attempt to set the body if the response may have content - if (MayHaveBody(statusCode, requestData.Method, contentLength)) - response = await SetBodyAsync(details, requestData, responseStream, mimeType, - cancellationToken).ConfigureAwait(false); + private static ApiCallDetails Initialize( + RequestData requestData, Exception exception, int? statusCode, Dictionary> headers, string mimeType, IReadOnlyDictionary threadPoolStats, + IReadOnlyDictionary tcpStats + ) + { + var success = false; + var allowedStatusCodes = requestData.AllowedStatusCodes; + if (statusCode.HasValue) + { + if (allowedStatusCodes.Contains(-1) || allowedStatusCodes.Contains(statusCode.Value)) + success = true; else - responseStream.Dispose(); - - response ??= new TResponse(); - - response.ApiCall = details; - return response; + success = requestData.ConnectionSettings + .StatusCodeToResponseSuccess(requestData.Method, statusCode.Value); } - /// - /// A helper which returns true if the response could potentially have a body. - /// - private static bool MayHaveBody(int? statusCode, HttpMethod httpMethod, long contentLength) => - contentLength != 0 && (!statusCode.HasValue || statusCode.Value != 204 && httpMethod != HttpMethod.HEAD); + //mimeType can include charset information on .NET full framework + if (!string.IsNullOrEmpty(mimeType) && !mimeType.StartsWith(requestData.Accept)) + success = false; - private static ApiCallDetails Initialize( - RequestData requestData, Exception exception, int? statusCode, Dictionary> headers, string mimeType, IReadOnlyDictionary threadPoolStats, - IReadOnlyDictionary tcpStats - ) + var details = new ApiCallDetails { - var success = false; - var allowedStatusCodes = requestData.AllowedStatusCodes; - if (statusCode.HasValue) - { - if (allowedStatusCodes.Contains(-1) || allowedStatusCodes.Contains(statusCode.Value)) - success = true; - else - success = requestData.ConnectionSettings - .StatusCodeToResponseSuccess(requestData.Method, statusCode.Value); - } + Success = success, + OriginalException = exception, + HttpStatusCode = statusCode, + RequestBodyInBytes = requestData.PostData?.WrittenBytes, + Uri = requestData.Uri, + HttpMethod = requestData.Method, + TcpStats = tcpStats, + ThreadPoolStats = threadPoolStats, + ResponseMimeType = mimeType, + TransportConfiguration = requestData.ConnectionSettings + }; - //mimeType can include charset information on .NET full framework - if (!string.IsNullOrEmpty(mimeType) && !mimeType.StartsWith(requestData.Accept)) - success = false; + if (headers is not null) + details.ParsedHeaders = new ReadOnlyDictionary>(headers); - var details = new ApiCallDetails - { - Success = success, - OriginalException = exception, - HttpStatusCode = statusCode, - RequestBodyInBytes = requestData.PostData?.WrittenBytes, - Uri = requestData.Uri, - HttpMethod = requestData.Method, - TcpStats = tcpStats, - ThreadPoolStats = threadPoolStats, - ResponseMimeType = mimeType, - TransportConfiguration = requestData.ConnectionSettings - }; - - if (headers is not null) - details.ParsedHeaders = new ReadOnlyDictionary>(headers); - - return details; - } + return details; + } - private TResponse SetBody(ApiCallDetails details, RequestData requestData, - Stream responseStream, string mimeType) - where TResponse : class, ITransportResponse, new() - { - byte[] bytes = null; + private TResponse SetBody(ApiCallDetails details, RequestData requestData, + Stream responseStream, string mimeType) + where TResponse : TransportResponse, new() + { + byte[] bytes = null; - var disableDirectStreaming = requestData.PostData?.DisableDirectStreaming ?? requestData.ConnectionSettings.DisableDirectStreaming; - var requiresErrorDeserialization = RequiresErrorDeserialization(details, requestData); + var disableDirectStreaming = requestData.PostData?.DisableDirectStreaming ?? requestData.ConnectionSettings.DisableDirectStreaming; + var requiresErrorDeserialization = RequiresErrorDeserialization(details, requestData); - if (disableDirectStreaming || NeedsToEagerReadStream() || requiresErrorDeserialization) - { - var inMemoryStream = requestData.MemoryStreamFactory.Create(); - responseStream.CopyTo(inMemoryStream, BufferSize); - bytes = SwapStreams(ref responseStream, ref inMemoryStream); - details.ResponseBodyInBytes = bytes; - } + if (disableDirectStreaming || NeedsToEagerReadStream() || requiresErrorDeserialization) + { + var inMemoryStream = requestData.MemoryStreamFactory.Create(); + responseStream.CopyTo(inMemoryStream, BufferSize); + bytes = SwapStreams(ref responseStream, ref inMemoryStream); + details.ResponseBodyInBytes = bytes; + } - using (responseStream) - { - if (SetSpecialTypes(mimeType, bytes, requestData.MemoryStreamFactory, out var r)) - return r; + using (responseStream) + { + if (SetSpecialTypes(mimeType, bytes, requestData.MemoryStreamFactory, out var r)) + return r; - if (details.HttpStatusCode.HasValue && - requestData.SkipDeserializationForStatusCodes.Contains(details.HttpStatusCode.Value)) - return null; + if (details.HttpStatusCode.HasValue && + requestData.SkipDeserializationForStatusCodes.Contains(details.HttpStatusCode.Value)) + return null; - var serializer = requestData.ConnectionSettings.RequestResponseSerializer; + var serializer = requestData.ConnectionSettings.RequestResponseSerializer; - if (requestData.CustomResponseBuilder != null) - return requestData.CustomResponseBuilder.DeserializeResponse(serializer, details, responseStream) as TResponse; + if (requestData.CustomResponseBuilder != null) + return requestData.CustomResponseBuilder.DeserializeResponse(serializer, details, responseStream) as TResponse; - // TODO: Handle empty data in a nicer way as throwing exceptions has a cost we'd like to avoid! - // ie. check content-length (add to ApiCallDetails)? - try - { - if (requiresErrorDeserialization && TryGetError(details, requestData, responseStream, out var error) && error.HasError()) - { - var response = new TResponse(); - SetErrorOnResponse(response, error); - return response; - } - - return mimeType == null || !mimeType.StartsWith(requestData.Accept, StringComparison.Ordinal) - ? null - : serializer.Deserialize(responseStream); - } - catch (JsonException ex) when (ex.Message.Contains("The input does not contain any JSON tokens")) + // TODO: Handle empty data in a nicer way as throwing exceptions has a cost we'd like to avoid! + // ie. check content-length (add to ApiCallDetails)? + try + { + if (requiresErrorDeserialization && TryGetError(details, requestData, responseStream, out var error) && error.HasError()) { - return default; + var response = new TResponse(); + SetErrorOnResponse(response, error); + return response; } - } - } - /// - /// - /// - /// - /// - /// - protected virtual bool RequiresErrorDeserialization(ApiCallDetails details, RequestData requestData) => false; - - /// - /// - /// - /// - /// - /// - /// - /// - protected virtual bool TryGetError(ApiCallDetails apiCallDetails, RequestData requestData, Stream responseStream, out TError? error) - { - if (!_isEmptyError) + return mimeType == null || !mimeType.StartsWith(requestData.Accept, StringComparison.Ordinal) + ? null + : serializer.Deserialize(responseStream); + } + catch (JsonException ex) when (ex.Message.Contains("The input does not contain any JSON tokens")) { - error = null; - return false; + return default; } + } + } - error = EmptyError.Instance as TError; + /// + /// + /// + /// + /// + /// + protected virtual bool RequiresErrorDeserialization(ApiCallDetails details, RequestData requestData) => false; - return error is not null; + /// + /// + /// + /// + /// + /// + /// + /// + protected virtual bool TryGetError(ApiCallDetails apiCallDetails, RequestData requestData, Stream responseStream, out TError? error) + { + if (!_isEmptyError) + { + error = null; + return false; } - /// - /// - /// - /// - /// - /// - protected virtual void SetErrorOnResponse(TResponse response, TError error) where TResponse : class, ITransportResponse, new() { } - - private async Task SetBodyAsync( - ApiCallDetails details, RequestData requestData, Stream responseStream, string mimeType, - CancellationToken cancellationToken - ) - where TResponse : class, ITransportResponse, new() - { - byte[] bytes = null; - var disableDirectStreaming = requestData.PostData?.DisableDirectStreaming ?? requestData.ConnectionSettings.DisableDirectStreaming; - var requiresErrorDeserialization = RequiresErrorDeserialization(details, requestData); + error = EmptyError.Instance as TError; - if (disableDirectStreaming || NeedsToEagerReadStream() || requiresErrorDeserialization) - { - var inMemoryStream = requestData.MemoryStreamFactory.Create(); - await responseStream.CopyToAsync(inMemoryStream, BufferSize, cancellationToken).ConfigureAwait(false); - bytes = SwapStreams(ref responseStream, ref inMemoryStream); - details.ResponseBodyInBytes = bytes; - } + return error is not null; + } - using (responseStream) - { - if (SetSpecialTypes(mimeType, bytes, requestData.MemoryStreamFactory, out var r)) return r; + /// + /// + /// + /// + /// + /// + protected virtual void SetErrorOnResponse(TResponse response, TError error) where TResponse : TransportResponse, new() { } + + private async Task SetBodyAsync( + ApiCallDetails details, RequestData requestData, Stream responseStream, string mimeType, + CancellationToken cancellationToken + ) + where TResponse : TransportResponse, new() + { + byte[] bytes = null; + var disableDirectStreaming = requestData.PostData?.DisableDirectStreaming ?? requestData.ConnectionSettings.DisableDirectStreaming; + var requiresErrorDeserialization = RequiresErrorDeserialization(details, requestData); - if (details.HttpStatusCode.HasValue && - requestData.SkipDeserializationForStatusCodes.Contains(details.HttpStatusCode.Value)) - return null; + if (disableDirectStreaming || NeedsToEagerReadStream() || requiresErrorDeserialization) + { + var inMemoryStream = requestData.MemoryStreamFactory.Create(); + await responseStream.CopyToAsync(inMemoryStream, BufferSize, cancellationToken).ConfigureAwait(false); + bytes = SwapStreams(ref responseStream, ref inMemoryStream); + details.ResponseBodyInBytes = bytes; + } - var serializer = requestData.ConnectionSettings.RequestResponseSerializer; + using (responseStream) + { + if (SetSpecialTypes(mimeType, bytes, requestData.MemoryStreamFactory, out var r)) return r; - if (requestData.CustomResponseBuilder != null) - return await requestData.CustomResponseBuilder - .DeserializeResponseAsync(serializer, details, responseStream, cancellationToken) - .ConfigureAwait(false) as TResponse; + if (details.HttpStatusCode.HasValue && + requestData.SkipDeserializationForStatusCodes.Contains(details.HttpStatusCode.Value)) + return null; - // TODO: Handle empty data in a nicer way as throwing exceptions has a cost we'd like to avoid! - // ie. check content-length (add to ApiCallDetails)? - try - { - if (requiresErrorDeserialization && TryGetError(details, requestData, responseStream, out var error) && error.HasError()) - { - var response = new TResponse(); - SetErrorOnResponse(response, error); - return response; - } - - return mimeType == null || !mimeType.StartsWith(requestData.Accept, StringComparison.Ordinal) - ? default - : await serializer - .DeserializeAsync(responseStream, cancellationToken) - .ConfigureAwait(false); - } - catch (JsonException ex) when (ex.Message.Contains("The input does not contain any JSON tokens")) + var serializer = requestData.ConnectionSettings.RequestResponseSerializer; + + if (requestData.CustomResponseBuilder != null) + return await requestData.CustomResponseBuilder + .DeserializeResponseAsync(serializer, details, responseStream, cancellationToken) + .ConfigureAwait(false) as TResponse; + + // TODO: Handle empty data in a nicer way as throwing exceptions has a cost we'd like to avoid! + // ie. check content-length (add to ApiCallDetails)? + try + { + if (requiresErrorDeserialization && TryGetError(details, requestData, responseStream, out var error) && error.HasError()) { - return default; + var response = new TResponse(); + SetErrorOnResponse(response, error); + return response; } + + return mimeType == null || !mimeType.StartsWith(requestData.Accept, StringComparison.Ordinal) + ? default + : await serializer + .DeserializeAsync(responseStream, cancellationToken) + .ConfigureAwait(false); + } + catch (JsonException ex) when (ex.Message.Contains("The input does not contain any JSON tokens")) + { + return default; } } + } - - private static bool SetSpecialTypes(string mimeType, byte[] bytes, - IMemoryStreamFactory memoryStreamFactory, out TResponse cs) - where TResponse : class, ITransportResponse, new() + + private static bool SetSpecialTypes(string mimeType, byte[] bytes, + MemoryStreamFactory memoryStreamFactory, out TResponse cs) + where TResponse : TransportResponse, new() + { + cs = null; + var responseType = typeof(TResponse); + if (!SpecialTypes.Contains(responseType)) return false; + + if (responseType == typeof(StringResponse)) + cs = new StringResponse(bytes.Utf8String()) as TResponse; + else if (responseType == typeof(BytesResponse)) + cs = new BytesResponse(bytes) as TResponse; + else if (responseType == typeof(VoidResponse)) + cs = VoidResponse.Default as TResponse; + else if (responseType == typeof(DynamicResponse)) { - cs = null; - var responseType = typeof(TResponse); - if (!SpecialTypes.Contains(responseType)) return false; - - if (responseType == typeof(StringResponse)) - cs = new StringResponse(bytes.Utf8String()) as TResponse; - else if (responseType == typeof(BytesResponse)) - cs = new BytesResponse(bytes) as TResponse; - else if (responseType == typeof(VoidResponse)) - cs = VoidResponse.Default as TResponse; - else if (responseType == typeof(DynamicResponse)) + //if not json store the result under "body" + if (mimeType == null || !mimeType.StartsWith(RequestData.MimeType)) { - //if not json store the result under "body" - if (mimeType == null || !mimeType.StartsWith(RequestData.MimeType)) + var dictionary = new DynamicDictionary { - var dictionary = new DynamicDictionary - { - ["body"] = new DynamicValue(bytes.Utf8String()) - }; - cs = new DynamicResponse(dictionary) as TResponse; - } - else - { - using var ms = memoryStreamFactory.Create(bytes); - var body = LowLevelRequestResponseSerializer.Instance.Deserialize(ms); - cs = new DynamicResponse(body) as TResponse; - } + ["body"] = new DynamicValue(bytes.Utf8String()) + }; + cs = new DynamicResponse(dictionary) as TResponse; + } + else + { + using var ms = memoryStreamFactory.Create(bytes); + var body = LowLevelRequestResponseSerializer.Instance.Deserialize(ms); + cs = new DynamicResponse(body) as TResponse; } - - return cs != null; } - private static bool NeedsToEagerReadStream() - where TResponse : class, ITransportResponse, new() => - typeof(TResponse) == typeof(StringResponse) - || typeof(TResponse) == typeof(BytesResponse) - || typeof(TResponse) == typeof(DynamicResponse); + return cs != null; + } + + private static bool NeedsToEagerReadStream() + where TResponse : TransportResponse, new() => + typeof(TResponse) == typeof(StringResponse) + || typeof(TResponse) == typeof(BytesResponse) + || typeof(TResponse) == typeof(DynamicResponse); - private static byte[] SwapStreams(ref Stream responseStream, ref MemoryStream ms) - { - var bytes = ms.ToArray(); - responseStream.Dispose(); - responseStream = ms; - responseStream.Position = 0; - return bytes; - } + private static byte[] SwapStreams(ref Stream responseStream, ref MemoryStream ms) + { + var bytes = ms.ToArray(); + responseStream.Dispose(); + responseStream = ms; + responseStream.Position = 0; + return bytes; } } diff --git a/src/Elastic.Transport/Components/Pipeline/IRequestPipeline.cs b/src/Elastic.Transport/Components/Pipeline/IRequestPipeline.cs deleted file mode 100644 index ca50215..0000000 --- a/src/Elastic.Transport/Components/Pipeline/IRequestPipeline.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Elastic.Transport.Diagnostics.Auditing; - -namespace Elastic.Transport -{ - /// Models the workflow of a request to multiple nodes - public interface IRequestPipeline : IDisposable // TODO - Should we move IDisposable to the implementation / Make this an abstract base type instead? - { - //TODO should not be List but requires a refactor - /// - /// An audit trail that can be used for logging and debugging purposes. Giving insights into how - /// the request made its way through the workflow - /// - List AuditTrail { get; } - - /// - /// Should the workflow attempt the initial sniff as requested by - /// - /// - bool FirstPoolUsageNeedsSniffing { get; } - -//TODO xmldocs -#pragma warning disable 1591 - bool IsTakingTooLong { get; } - - int MaxRetries { get; } - - int Retried { get; } - - bool SniffsOnConnectionFailure { get; } - - bool SniffsOnStaleCluster { get; } - - bool StaleClusterState { get; } - - DateTime StartedOn { get; } - - TResponse CallProductEndpoint(RequestData requestData) - where TResponse : class, ITransportResponse, new(); - - Task CallProductEndpointAsync(RequestData requestData, CancellationToken cancellationToken) - where TResponse : class, ITransportResponse, new(); - - void MarkAlive(Node node); - - void MarkDead(Node node); - - /// - /// Attempt to get a single node when the underlying connection pool contains only one node. - /// - /// This provides an optimised path for single node pools by avoiding an Enumerator on each call. - /// - /// - /// - /// true when a single node exists which has been set on the . - bool TryGetSingleNode(out Node node); - - IEnumerable NextNode(); - - void Ping(Node node); - - Task PingAsync(Node node, CancellationToken cancellationToken); - - void FirstPoolUsage(SemaphoreSlim semaphore); - - Task FirstPoolUsageAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken); - - void Sniff(); - - Task SniffAsync(CancellationToken cancellationToken); - - void SniffOnStaleCluster(); - - Task SniffOnStaleClusterAsync(CancellationToken cancellationToken); - - void SniffOnConnectionFailure(); - - Task SniffOnConnectionFailureAsync(CancellationToken cancellationToken); - - void BadResponse(ref TResponse response, IApiCallDetails callDetails, RequestData data, TransportException exception) - where TResponse : class, ITransportResponse, new(); - - void ThrowNoNodesAttempted(RequestData requestData, List seenExceptions); - - void AuditCancellationRequested(); - - TransportException CreateClientException(TResponse response, IApiCallDetails callDetails, RequestData data, - List seenExceptions - ) - where TResponse : class, ITransportResponse, new(); -#pragma warning restore 1591 - } -} diff --git a/src/Elastic.Transport/Components/Pipeline/PipelineException.cs b/src/Elastic.Transport/Components/Pipeline/PipelineException.cs index 6e44554..aeeb988 100644 --- a/src/Elastic.Transport/Components/Pipeline/PipelineException.cs +++ b/src/Elastic.Transport/Components/Pipeline/PipelineException.cs @@ -4,63 +4,51 @@ using System; -namespace Elastic.Transport -{ - /// - /// A pipeline exception is throw when ever a known failing exit point is reached in - /// See for known exits points - /// - public class PipelineException : Exception - { - /// - public PipelineException(PipelineFailure failure) - : base(GetMessage(failure)) => FailureReason = failure; - - /// - public PipelineException(PipelineFailure failure, Exception innerException) - : base(GetMessage(failure), innerException) => FailureReason = failure; +namespace Elastic.Transport; - /// - public PipelineFailure FailureReason { get; } +/// +/// A pipeline exception is throw when ever a known failing exit point is reached in +/// See for known exits points +/// +public class PipelineException : Exception +{ + /// + public PipelineException(PipelineFailure failure) + : base(GetMessage(failure)) => FailureReason = failure; - /// - /// This exception is one the can handle - /// - /// - /// - /// - public bool Recoverable => - FailureReason == PipelineFailure.BadRequest - || FailureReason == PipelineFailure.BadResponse - || FailureReason == PipelineFailure.PingFailure; + /// + public PipelineException(PipelineFailure failure, Exception innerException) + : base(GetMessage(failure), innerException) => FailureReason = failure; - //TODO why do we have both Response and ApiCall? + /// + public PipelineFailure FailureReason { get; } - /// The response that triggered this exception - public ITransportResponse Response { get; internal set; } + /// + /// This exception is one the can handle + /// + /// + /// + /// + public bool Recoverable => + FailureReason == PipelineFailure.BadRequest + || FailureReason == PipelineFailure.BadResponse + || FailureReason == PipelineFailure.PingFailure; - /// The response that triggered this exception - public IApiCallDetails ApiCall { get; internal set; } + /// The response that triggered this exception + public TransportResponse Response { get; internal set; } - private static string GetMessage(PipelineFailure failure) + private static string GetMessage(PipelineFailure failure) => + failure switch { - switch (failure) - { - case PipelineFailure.BadRequest: return "An error occurred trying to write the request data to the specified node."; - case PipelineFailure.BadResponse: return "An error occurred trying to read the response from the specified node."; - case PipelineFailure.BadAuthentication: - return "Could not authenticate with the specified node. Try verifying your credentials or check your Shield configuration."; - case PipelineFailure.PingFailure: return "Failed to ping the specified node."; - case PipelineFailure.SniffFailure: return "Failed sniffing cluster state."; - case PipelineFailure.CouldNotStartSniffOnStartup: return "Failed sniffing cluster state upon client startup."; - case PipelineFailure.MaxTimeoutReached: return "Maximum timeout was reached."; - case PipelineFailure.MaxRetriesReached: return "The call was retried the configured maximum amount of times"; - case PipelineFailure.NoNodesAttempted: - return "No nodes were attempted, this can happen when a node predicate does not match any nodes"; - case PipelineFailure.Unexpected: - default: - return "An unexpected error occurred. Try checking the original exception for more information."; - } - } - } + PipelineFailure.BadRequest => "An error occurred trying to write the request data to the specified node.", + PipelineFailure.BadResponse => "An error occurred trying to read the response from the specified node.", + PipelineFailure.BadAuthentication => "Could not authenticate with the specified node. Try verifying your credentials or check your Shield configuration.", + PipelineFailure.PingFailure => "Failed to ping the specified node.", + PipelineFailure.SniffFailure => "Failed sniffing cluster state.", + PipelineFailure.CouldNotStartSniffOnStartup => "Failed sniffing cluster state upon client startup.", + PipelineFailure.MaxTimeoutReached => "Maximum timeout was reached.", + PipelineFailure.MaxRetriesReached => "The call was retried the configured maximum amount of times", + PipelineFailure.NoNodesAttempted => "No nodes were attempted, this can happen when a node predicate does not match any nodes", + _ => "An unexpected error occurred. Try checking the original exception for more information.", + }; } diff --git a/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs b/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs index 317e473..be86684 100644 --- a/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs +++ b/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs @@ -4,56 +4,55 @@ using Elastic.Transport.Products; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A failure in 's workflow that caused it to end prematurely. +/// +public enum PipelineFailure { /// - /// A failure in 's workflow that caused it to end prematurely. + /// The provided credentials were insufficient. + /// If this is thrown during an initial sniff or ping it short circuits and returns immediately + /// + BadAuthentication, + + /// + /// A bad response as determined by + /// + BadResponse, + + /// A ping request was unsuccessful + PingFailure, + /// A sniff request was unsuccessful + SniffFailure, + + /// + /// See was requested but the first API call failed to sniff + /// + CouldNotStartSniffOnStartup, + + /// + /// The overall timeout specified by was reached + /// + MaxTimeoutReached, + + /// + /// The overall max retries as specified by was reached + /// + MaxRetriesReached, + + /// + /// An exception occurred during that could not be handled + /// + Unexpected, + + /// An exception happened while sending the request and a response was never fetched + BadRequest, + + /// + /// Rare but if is too stringent it could mean no + /// nodes were considered for the API call /// - public enum PipelineFailure - { - /// - /// The provided credentials were insufficient. - /// If this is thrown during an initial sniff or ping it short circuits and returns immediately - /// - BadAuthentication, - - /// - /// A bad response as determined by - /// - BadResponse, - - /// A ping request was unsuccessful - PingFailure, - /// A sniff request was unsuccessful - SniffFailure, - - /// - /// See was requested but the first API call failed to sniff - /// - CouldNotStartSniffOnStartup, - - /// - /// The overall timeout specified by was reached - /// - MaxTimeoutReached, - - /// - /// The overall max retries as specified by was reached - /// - MaxRetriesReached, - - /// - /// An exception occurred during that could not be handled - /// - Unexpected, - - /// An exception happened while sending the request and a response was never fetched - BadRequest, - - /// - /// Rare but if is too stringent it could mean no - /// nodes were considered for the API call - /// - NoNodesAttempted - } + NoNodesAttempted } diff --git a/src/Elastic.Transport/Components/Pipeline/RequestData.cs b/src/Elastic.Transport/Components/Pipeline/RequestData.cs index 8b31a07..9d02054 100644 --- a/src/Elastic.Transport/Components/Pipeline/RequestData.cs +++ b/src/Elastic.Transport/Components/Pipeline/RequestData.cs @@ -5,226 +5,224 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Security; using System.Security.Cryptography.X509Certificates; using Elastic.Transport.Diagnostics.Auditing; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Where and how should connect to. +/// +/// Represents the cumulative configuration from +/// and . +/// +/// +public sealed class RequestData { - /// - /// Where and how should connect to. - /// - /// Represents the cumulative configuration from - /// and . - /// - /// - public sealed class RequestData - { //TODO add xmldocs and clean up this class #pragma warning disable 1591 - public const string MimeType = "application/json"; - public const string MimeTypeTextPlain = "text/plain"; - public const string OpaqueIdHeader = "X-Opaque-Id"; - public const string RunAsSecurityHeader = "es-security-runas-user"; - - private Uri _requestUri; - private Node _node; - - public RequestData( - HttpMethod method, string path, - PostData data, - ITransportConfiguration global, - IRequestParameters local, - IMemoryStreamFactory memoryStreamFactory - ) - : this(method, data, global, local?.RequestConfiguration, memoryStreamFactory) + public const string MimeType = "application/json"; + public const string MimeTypeTextPlain = "text/plain"; + public const string OpaqueIdHeader = "X-Opaque-Id"; + public const string RunAsSecurityHeader = "es-security-runas-user"; + + private Uri _requestUri; + private Node _node; + + public RequestData( + HttpMethod method, string path, + PostData data, + ITransportConfiguration global, + RequestParameters local, + MemoryStreamFactory memoryStreamFactory + ) + : this(method, data, global, local?.RequestConfiguration, memoryStreamFactory) + { + _path = path; + CustomResponseBuilder = local?.CustomResponseBuilder; + PathAndQuery = CreatePathWithQueryStrings(path, ConnectionSettings, local); + } + + private RequestData(HttpMethod method, + PostData data, + ITransportConfiguration global, + IRequestConfiguration local, + MemoryStreamFactory memoryStreamFactory + ) + { + ConnectionSettings = global; + MemoryStreamFactory = memoryStreamFactory; + Method = method; + PostData = data; + + if (data != null) + data.DisableDirectStreaming = local?.DisableDirectStreaming ?? global.DisableDirectStreaming; + + Pipelined = local?.EnableHttpPipelining ?? global.HttpPipeliningEnabled; + HttpCompression = global.EnableHttpCompression; + RequestMimeType = local?.ContentType ?? MimeType; + Accept = local?.Accept ?? MimeType; + + if (global.Headers != null) + Headers = new NameValueCollection(global.Headers); + + if (local?.Headers != null) + { + Headers ??= new NameValueCollection(); + foreach (var key in local.Headers.AllKeys) + Headers[key] = local.Headers[key]; + } + + if (!string.IsNullOrEmpty(local?.OpaqueId)) { - _path = path; - CustomResponseBuilder = local?.CustomResponseBuilder; - PathAndQuery = CreatePathWithQueryStrings(path, ConnectionSettings, local); + Headers ??= new NameValueCollection(); + Headers.Add(OpaqueIdHeader, local.OpaqueId); } - private RequestData(HttpMethod method, - PostData data, - ITransportConfiguration global, - IRequestConfiguration local, - IMemoryStreamFactory memoryStreamFactory - ) + RunAs = local?.RunAs; + SkipDeserializationForStatusCodes = global.SkipDeserializationForStatusCodes; + ThrowExceptions = local?.ThrowExceptions ?? global.ThrowExceptions; + + RequestTimeout = local?.RequestTimeout ?? global.RequestTimeout; + PingTimeout = + local?.PingTimeout + ?? global.PingTimeout + ?? (global.NodePool.UsingSsl ? TransportConfiguration.DefaultPingTimeoutOnSsl : TransportConfiguration.DefaultPingTimeout); + + KeepAliveInterval = (int)(global.KeepAliveInterval?.TotalMilliseconds ?? 2000); + KeepAliveTime = (int)(global.KeepAliveTime?.TotalMilliseconds ?? 2000); + DnsRefreshTimeout = global.DnsRefreshTimeout; + + MetaHeaderProvider = global.MetaHeaderProvider; + RequestMetaData = local?.RequestMetaData?.Items ?? EmptyReadOnly.Dictionary; + + ProxyAddress = global.ProxyAddress; + ProxyUsername = global.ProxyUsername; + ProxyPassword = global.ProxyPassword; + DisableAutomaticProxyDetection = global.DisableAutomaticProxyDetection; + AuthenticationHeader = local?.AuthenticationHeader ?? global.Authentication; + AllowedStatusCodes = local?.AllowedStatusCodes ?? EmptyReadOnly.Collection; + ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates; + UserAgent = global.UserAgent; + TransferEncodingChunked = local?.TransferEncodingChunked ?? global.TransferEncodingChunked; + TcpStats = local?.EnableTcpStats ?? global.EnableTcpStats; + ThreadPoolStats = local?.EnableThreadPoolStats ?? global.EnableThreadPoolStats; + ParseAllHeaders = local?.ParseAllHeaders ?? global.ParseAllHeaders ?? false; + + if (local is not null) { - ConnectionSettings = global; - MemoryStreamFactory = memoryStreamFactory; - Method = method; - PostData = data; - - if (data != null) - data.DisableDirectStreaming = local?.DisableDirectStreaming ?? global.DisableDirectStreaming; - - Pipelined = local?.EnableHttpPipelining ?? global.HttpPipeliningEnabled; - HttpCompression = global.EnableHttpCompression; - RequestMimeType = local?.ContentType ?? MimeType; - Accept = local?.Accept ?? MimeType; - - if (global.Headers != null) - Headers = new NameValueCollection(global.Headers); - - if (local?.Headers != null) - { - Headers ??= new NameValueCollection(); - foreach (var key in local.Headers.AllKeys) - Headers[key] = local.Headers[key]; - } - - if (!string.IsNullOrEmpty(local?.OpaqueId)) - { - Headers ??= new NameValueCollection(); - Headers.Add(OpaqueIdHeader, local.OpaqueId); - } - - RunAs = local?.RunAs; - SkipDeserializationForStatusCodes = global.SkipDeserializationForStatusCodes; - ThrowExceptions = local?.ThrowExceptions ?? global.ThrowExceptions; - - RequestTimeout = local?.RequestTimeout ?? global.RequestTimeout; - PingTimeout = - local?.PingTimeout - ?? global.PingTimeout - ?? (global.NodePool.UsingSsl ? TransportConfiguration.DefaultPingTimeoutOnSsl : TransportConfiguration.DefaultPingTimeout); - - KeepAliveInterval = (int)(global.KeepAliveInterval?.TotalMilliseconds ?? 2000); - KeepAliveTime = (int)(global.KeepAliveTime?.TotalMilliseconds ?? 2000); - DnsRefreshTimeout = global.DnsRefreshTimeout; - - MetaHeaderProvider = global.MetaHeaderProvider; - RequestMetaData = local?.RequestMetaData?.Items ?? EmptyReadOnly.Dictionary; - - ProxyAddress = global.ProxyAddress; - ProxyUsername = global.ProxyUsername; - ProxyPassword = global.ProxyPassword; - DisableAutomaticProxyDetection = global.DisableAutomaticProxyDetection; - AuthenticationHeader = local?.AuthenticationHeader ?? global.Authentication; - AllowedStatusCodes = local?.AllowedStatusCodes ?? EmptyReadOnly.Collection; - ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates; - UserAgent = global.UserAgent; - TransferEncodingChunked = local?.TransferEncodingChunked ?? global.TransferEncodingChunked; - TcpStats = local?.EnableTcpStats ?? global.EnableTcpStats; - ThreadPoolStats = local?.EnableThreadPoolStats ?? global.EnableThreadPoolStats; - ParseAllHeaders = local?.ParseAllHeaders ?? global.ParseAllHeaders ?? false; - - if (local is not null) - { - ResponseHeadersToParse = local.ResponseHeadersToParse; - ResponseHeadersToParse = new HeadersList(local.ResponseHeadersToParse, global.ResponseHeadersToParse); - } - else - { - ResponseHeadersToParse = global.ResponseHeadersToParse; - } + ResponseHeadersToParse = local.ResponseHeadersToParse; + ResponseHeadersToParse = new HeadersList(local.ResponseHeadersToParse, global.ResponseHeadersToParse); } + else + { + ResponseHeadersToParse = global.ResponseHeadersToParse; + } + } - private readonly string _path; - - public string Accept { get; } - public IReadOnlyCollection AllowedStatusCodes { get; } - public AuthorizationHeader AuthenticationHeader { get; } - public X509CertificateCollection ClientCertificates { get; } - public ITransportConfiguration ConnectionSettings { get; } - public CustomResponseBuilder CustomResponseBuilder { get; } - public bool DisableAutomaticProxyDetection { get; } - public HeadersList ResponseHeadersToParse { get; } - public bool ParseAllHeaders { get; } - public NameValueCollection Headers { get; } - public bool HttpCompression { get; } - public int KeepAliveInterval { get; } - public int KeepAliveTime { get; } - public bool MadeItToResponse { get; set; } - public IMemoryStreamFactory MemoryStreamFactory { get; } - public HttpMethod Method { get; } - - public Node Node + private readonly string _path; + + public string Accept { get; } + public IReadOnlyCollection AllowedStatusCodes { get; } + public AuthorizationHeader AuthenticationHeader { get; } + public X509CertificateCollection ClientCertificates { get; } + public ITransportConfiguration ConnectionSettings { get; } + public CustomResponseBuilder CustomResponseBuilder { get; } + public bool DisableAutomaticProxyDetection { get; } + public HeadersList ResponseHeadersToParse { get; } + public bool ParseAllHeaders { get; } + public NameValueCollection Headers { get; } + public bool HttpCompression { get; } + public int KeepAliveInterval { get; } + public int KeepAliveTime { get; } + public bool MadeItToResponse { get; set; } + public MemoryStreamFactory MemoryStreamFactory { get; } + public HttpMethod Method { get; } + + public Node Node + { + get => _node; + set { - get => _node; - set - { - // We want the Uri to regenerate when the node changes - _requestUri = null; - _node = value; - } + // We want the Uri to regenerate when the node changes + _requestUri = null; + _node = value; } + } + + public AuditEvent OnFailureAuditEvent => MadeItToResponse ? AuditEvent.BadResponse : AuditEvent.BadRequest; + public PipelineFailure OnFailurePipelineFailure => MadeItToResponse ? PipelineFailure.BadResponse : PipelineFailure.BadRequest; + public string PathAndQuery { get; } + public TimeSpan PingTimeout { get; } + public bool Pipelined { get; } + public PostData PostData { get; } + public string ProxyAddress { get; } + public string ProxyPassword { get; } + public string ProxyUsername { get; } + // TODO: rename to ContentType in 8.0.0 + public string RequestMimeType { get; } + public TimeSpan RequestTimeout { get; } + public string RunAs { get; } + public IReadOnlyCollection SkipDeserializationForStatusCodes { get; } + public bool ThrowExceptions { get; } + public UserAgent UserAgent { get; } + public bool TransferEncodingChunked { get; } + public bool TcpStats { get; } + public bool ThreadPoolStats { get; } - public AuditEvent OnFailureAuditEvent => MadeItToResponse ? AuditEvent.BadResponse : AuditEvent.BadRequest; - public PipelineFailure OnFailurePipelineFailure => MadeItToResponse ? PipelineFailure.BadResponse : PipelineFailure.BadRequest; - public string PathAndQuery { get; } - public TimeSpan PingTimeout { get; } - public bool Pipelined { get; } - public PostData PostData { get; } - public string ProxyAddress { get; } - public string ProxyPassword { get; } - public string ProxyUsername { get; } - // TODO: rename to ContentType in 8.0.0 - public string RequestMimeType { get; } - public TimeSpan RequestTimeout { get; } - public string RunAs { get; } - public IReadOnlyCollection SkipDeserializationForStatusCodes { get; } - public bool ThrowExceptions { get; } - public UserAgent UserAgent { get; } - public bool TransferEncodingChunked { get; } - public bool TcpStats { get; } - public bool ThreadPoolStats { get; } - - /// - /// The for the request. - /// - public Uri Uri + /// + /// The for the request. + /// + public Uri Uri + { + get { - get - { - if (_requestUri is not null) return _requestUri; + if (_requestUri is not null) return _requestUri; - _requestUri = Node is not null ? new Uri(Node.Uri, PathAndQuery) : null; - return _requestUri; - } + _requestUri = Node is not null ? new Uri(Node.Uri, PathAndQuery) : null; + return _requestUri; } + } - public TimeSpan DnsRefreshTimeout { get; } + public TimeSpan DnsRefreshTimeout { get; } - public MetaHeaderProvider MetaHeaderProvider { get; } + public MetaHeaderProvider MetaHeaderProvider { get; } - public IReadOnlyDictionary RequestMetaData { get; } + public IReadOnlyDictionary RequestMetaData { get; } - public bool IsAsync { get; internal set; } + public bool IsAsync { get; internal set; } - public override string ToString() => $"{Method.GetStringValue()} {_path}"; + public override string ToString() => $"{Method.GetStringValue()} {_path}"; - // TODO This feels like its in the wrong place - private string CreatePathWithQueryStrings(string path, ITransportConfiguration global, IRequestParameters request) - { - path ??= string.Empty; - if (path.Contains("?")) - throw new ArgumentException($"{nameof(path)} can not contain querystring parameters and needs to be already escaped"); + // TODO This feels like its in the wrong place + private string CreatePathWithQueryStrings(string path, ITransportConfiguration global, RequestParameters request) + { + path ??= string.Empty; + if (path.Contains("?")) + throw new ArgumentException($"{nameof(path)} can not contain querystring parameters and needs to be already escaped"); - var g = global.QueryStringParameters; - var l = request?.QueryString; + var g = global.QueryStringParameters; + var l = request?.QueryString; - if ((g == null || g.Count == 0) && (l == null || l.Count == 0)) return path; + if ((g == null || g.Count == 0) && (l == null || l.Count == 0)) return path; - //create a copy of the global query string collection if needed. - var nv = g == null ? new NameValueCollection() : new NameValueCollection(g); + //create a copy of the global query string collection if needed. + var nv = g == null ? new NameValueCollection() : new NameValueCollection(g); - //set all querystring pairs from local `l` on the querystring collection - var formatter = ConnectionSettings.UrlFormatter; - nv.UpdateFromDictionary(l, formatter); + //set all querystring pairs from local `l` on the querystring collection + var formatter = ConnectionSettings.UrlFormatter; + nv.UpdateFromDictionary(l, formatter); - //if nv has no keys simply return path as provided - if (!nv.HasKeys()) return path; + //if nv has no keys simply return path as provided + if (!nv.HasKeys()) return path; - //create string for query string collection where key and value are escaped properly. - var queryString = ToQueryString(nv); - path += queryString; - return path; - } + //create string for query string collection where key and value are escaped properly. + var queryString = ToQueryString(nv); + path += queryString; + return path; + } - public static string ToQueryString(NameValueCollection collection) => collection.ToQueryString(); + public static string ToQueryString(NameValueCollection collection) => collection.ToQueryString(); #pragma warning restore 1591 - } } diff --git a/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs b/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs index 2296e9e..8bdeef6 100644 --- a/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs +++ b/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs @@ -4,630 +4,130 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Elastic.Transport.Diagnostics; using Elastic.Transport.Diagnostics.Auditing; -using Elastic.Transport.Extensions; -using Elastic.Transport.Products; -using static Elastic.Transport.Diagnostics.Auditing.AuditEvent; -//#if NETSTANDARD2_0 || NETSTANDARD2_1 -//using System.Threading.Tasks.Extensions; -//#endif +namespace Elastic.Transport; -namespace Elastic.Transport +/// +/// Models the workflow of a request to multiple nodes +/// +public abstract class RequestPipeline : IDisposable { - internal static class RequestPipelineStatics - { - public static readonly string NoNodesAttemptedMessage = - "No nodes were attempted, this can happen when a node predicate does not match any nodes"; - - public static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.RequestPipeline.SourceName); - } + private bool _disposed = false; - /// - public class RequestPipeline : IRequestPipeline - where TConfiguration : class, ITransportConfiguration - { - private readonly ITransportClient _transportClient; - private readonly NodePool _nodePool; - private readonly IDateTimeProvider _dateTimeProvider; - private readonly IMemoryStreamFactory _memoryStreamFactory; - private readonly Func _nodePredicate; - private readonly IProductRegistration _productRegistration; - private readonly TConfiguration _settings; - private readonly ResponseBuilder _responseBuilder; - - private RequestConfiguration _pingAndSniffRequestConfiguration; - - /// - public RequestPipeline( - TConfiguration configurationValues, - IDateTimeProvider dateTimeProvider, - IMemoryStreamFactory memoryStreamFactory, - IRequestParameters requestParameters - ) - { - _settings = configurationValues; - _nodePool = _settings.NodePool; - _transportClient = _settings.Connection; - _dateTimeProvider = dateTimeProvider; - _memoryStreamFactory = memoryStreamFactory; - _productRegistration = configurationValues.ProductRegistration; - _responseBuilder = _productRegistration.ResponseBuilder; - _nodePredicate = _settings.NodePredicate ?? _productRegistration.NodePredicate; - - RequestConfiguration = requestParameters?.RequestConfiguration; - StartedOn = dateTimeProvider.Now(); - } + internal RequestPipeline() { } - /// - public List AuditTrail { get; } = new(); + //TODO should not be List but requires a refactor + /// + /// An audit trail that can be used for logging and debugging purposes. Giving insights into how + /// the request made its way through the workflow + /// + public abstract List AuditTrail { get; } - private RequestConfiguration PingAndSniffRequestConfiguration - { - // Lazily loaded when first required, since not all node pools and configurations support pinging and sniffing. - // This avoids allocating 192B per request for those which do not need to ping or sniff. - get - { - if (_pingAndSniffRequestConfiguration is not null) return _pingAndSniffRequestConfiguration; - - _pingAndSniffRequestConfiguration = new RequestConfiguration - { - PingTimeout = PingTimeout, - RequestTimeout = PingTimeout, - AuthenticationHeader = _settings.Authentication, - EnableHttpPipelining = RequestConfiguration?.EnableHttpPipelining ?? _settings.HttpPipeliningEnabled, - ForceNode = RequestConfiguration?.ForceNode - }; - - return _pingAndSniffRequestConfiguration; - } - } + /// + /// Should the workflow attempt the initial sniff as requested by + /// + /// + public abstract bool FirstPoolUsageNeedsSniffing { get; } - //TODO xmldocs + //TODO xmldocs #pragma warning disable 1591 - public bool DepletedRetries => Retried >= MaxRetries + 1 || IsTakingTooLong; + public abstract bool IsTakingTooLong { get; } - public bool FirstPoolUsageNeedsSniffing => - !RequestDisabledSniff - && _nodePool.SupportsReseeding && _settings.SniffsOnStartup && !_nodePool.SniffedOnStartup; + public abstract int MaxRetries { get; } - public bool IsTakingTooLong - { - get - { - var timeout = _settings.MaxRetryTimeout.GetValueOrDefault(RequestTimeout); - var now = _dateTimeProvider.Now(); - - //we apply a soft margin so that if a request timesout at 59 seconds when the maximum is 60 we also abort. - var margin = timeout.TotalMilliseconds / 100.0 * 98; - var marginTimeSpan = TimeSpan.FromMilliseconds(margin); - var timespanCall = now - StartedOn; - var tookToLong = timespanCall >= marginTimeSpan; - return tookToLong; - } - } + public abstract bool SniffsOnConnectionFailure { get; } - public int MaxRetries => - RequestConfiguration?.ForceNode != null - ? 0 - : Math.Min(RequestConfiguration?.MaxRetries ?? _settings.MaxRetries.GetValueOrDefault(int.MaxValue), _nodePool.MaxRetries); + public abstract bool SniffsOnStaleCluster { get; } - public bool Refresh { get; private set; } - public int Retried { get; private set; } + public abstract bool StaleClusterState { get; } - public IEnumerable SniffNodes => _nodePool - .CreateView(LazyAuditable) - .ToList() - .OrderBy(n => _productRegistration.SniffOrder(n)); + public abstract DateTime StartedOn { get; } - public bool SniffsOnConnectionFailure => - !RequestDisabledSniff - && _nodePool.SupportsReseeding && _settings.SniffsOnConnectionFault; + public abstract TResponse CallProductEndpoint(RequestData requestData) + where TResponse : TransportResponse, new(); - public bool SniffsOnStaleCluster => - !RequestDisabledSniff - && _nodePool.SupportsReseeding && _settings.SniffInformationLifeSpan.HasValue; + public abstract Task CallProductEndpointAsync(RequestData requestData, CancellationToken cancellationToken) + where TResponse : TransportResponse, new(); - public bool StaleClusterState - { - get - { - if (!SniffsOnStaleCluster) return false; - - // ReSharper disable once PossibleInvalidOperationException - // already checked by SniffsOnStaleCluster - var sniffLifeSpan = _settings.SniffInformationLifeSpan.Value; - - var now = _dateTimeProvider.Now(); - var lastSniff = _nodePool.LastUpdate; - - return sniffLifeSpan < now - lastSniff; - } - } + public abstract void MarkAlive(Node node); - public DateTime StartedOn { get; } + public abstract void MarkDead(Node node); - private TimeSpan PingTimeout => - RequestConfiguration?.PingTimeout - ?? _settings.PingTimeout - ?? (_nodePool.UsingSsl ? TransportConfiguration.DefaultPingTimeoutOnSsl : TransportConfiguration.DefaultPingTimeout); + /// + /// Attempt to get a single node when the underlying connection pool contains only one node. + /// + /// This provides an optimised path for single node pools by avoiding an Enumerator on each call. + /// + /// + /// + /// true when a single node exists which has been set on the . + public abstract bool TryGetSingleNode(out Node node); - private IRequestConfiguration RequestConfiguration { get; } + public abstract IEnumerable NextNode(); - private bool RequestDisabledSniff => RequestConfiguration != null && (RequestConfiguration.DisableSniff ?? false); + public abstract void Ping(Node node); - private TimeSpan RequestTimeout => RequestConfiguration?.RequestTimeout ?? _settings.RequestTimeout; - - void IDisposable.Dispose() => Dispose(); - - public void AuditCancellationRequested() => Audit(CancellationRequested).Dispose(); - - public void BadResponse(ref TResponse response, IApiCallDetails callDetails, RequestData data, - TransportException exception - ) - where TResponse : class, ITransportResponse, new() - { - if (response == null) - { - //make sure we copy over the error body in case we disabled direct streaming. - var s = callDetails?.ResponseBodyInBytes == null ? Stream.Null : _memoryStreamFactory.Create(callDetails.ResponseBodyInBytes); - var m = callDetails?.ResponseMimeType ?? RequestData.MimeType; - response = _responseBuilder.ToResponse(data, exception, callDetails?.HttpStatusCode, null, s, m, callDetails?.ResponseBodyInBytes?.Length ?? -1, null, null); - } - - if (response.ApiCall is ApiCallDetails apiCallDetails) - apiCallDetails.AuditTrail = AuditTrail; - } + public abstract Task PingAsync(Node node, CancellationToken cancellationToken); - public TResponse CallProductEndpoint(RequestData requestData) - where TResponse : class, ITransportResponse, new() - { - using (var audit = Audit(HealthyResponse, requestData.Node)) - using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose( - DiagnosticSources.RequestPipeline.CallProductEndpoint, requestData)) - { - audit.Path = requestData.PathAndQuery; - try - { - var response = _transportClient.Request(requestData); - d.EndState = response.ApiCall; - - if (response.ApiCall is ApiCallDetails apiCallDetails) - apiCallDetails.AuditTrail = AuditTrail; - - ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCall, response); - if (!response.ApiCall.Success) audit.Event = requestData.OnFailureAuditEvent; - return response; - } - catch (Exception e) - { - audit.Event = requestData.OnFailureAuditEvent; - audit.Exception = e; - throw; - } - } - } + public abstract void FirstPoolUsage(SemaphoreSlim semaphore); - public async Task CallProductEndpointAsync(RequestData requestData, CancellationToken cancellationToken) - where TResponse : class, ITransportResponse, new() - { - using (var audit = Audit(HealthyResponse, requestData.Node)) - using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose( - DiagnosticSources.RequestPipeline.CallProductEndpoint, requestData)) - { - audit.Path = requestData.PathAndQuery; - try - { - var response = await _transportClient.RequestAsync(requestData, cancellationToken).ConfigureAwait(false); - d.EndState = response.ApiCall; - - if (response.ApiCall is ApiCallDetails apiCallDetails) - apiCallDetails.AuditTrail = AuditTrail; - - ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCall, response); - if (!response.ApiCall.Success) audit.Event = requestData.OnFailureAuditEvent; - return response; - } - catch (Exception e) - { - audit.Event = requestData.OnFailureAuditEvent; - audit.Exception = e; - throw; - } - } - } + public abstract Task FirstPoolUsageAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken); - public TransportException CreateClientException( - TResponse response, IApiCallDetails callDetails, RequestData data, List pipelineExceptions - ) - where TResponse : class, ITransportResponse, new() - { - if (callDetails?.Success ?? false) return null; - - var pipelineFailure = data.OnFailurePipelineFailure; - var innerException = callDetails?.OriginalException; - if (pipelineExceptions.HasAny(out var exs)) - { - pipelineFailure = exs.Last().FailureReason; - innerException = exs.AsAggregateOrFirst(); - } - - var statusCode = callDetails?.HttpStatusCode != null ? callDetails.HttpStatusCode.Value.ToString() : "unknown"; - var resource = callDetails == null - ? "unknown resource" - : $"Status code {statusCode} from: {callDetails.HttpMethod} {callDetails.Uri.PathAndQuery}"; - - var exceptionMessage = innerException?.Message ?? "Request failed to execute"; - - if (IsTakingTooLong) - { - pipelineFailure = PipelineFailure.MaxTimeoutReached; - Audit(MaxTimeoutReached); - exceptionMessage = "Maximum timeout reached while retrying request"; - } - else if (Retried >= MaxRetries && MaxRetries > 0) - { - pipelineFailure = PipelineFailure.MaxRetriesReached; - Audit(MaxRetriesReached); - exceptionMessage = "Maximum number of retries reached"; - - var now = _dateTimeProvider.Now(); - var activeNodes = _nodePool.Nodes.Count(n => n.IsAlive || n.DeadUntil <= now); - if (Retried >= activeNodes) - { - Audit(FailedOverAllNodes); - exceptionMessage += ", failed over to all the known alive nodes before failing"; - } - } - - exceptionMessage += !exceptionMessage.EndsWith(".", StringComparison.Ordinal) ? $". Call: {resource}" : $" Call: {resource}"; - if (response != null && _productRegistration.TryGetServerErrorReason(response, out var reason)) - exceptionMessage += $". ServerError: {reason}"; - - var clientException = new TransportException(pipelineFailure, exceptionMessage, innerException) - { - Request = data, Response = callDetails, AuditTrail = AuditTrail - }; - - return clientException; - } + public abstract void Sniff(); - public void FirstPoolUsage(SemaphoreSlim semaphore) - { - if (!FirstPoolUsageNeedsSniffing) return; - - if (!semaphore.Wait(_settings.RequestTimeout)) - { - if (FirstPoolUsageNeedsSniffing) - throw new PipelineException(PipelineFailure.CouldNotStartSniffOnStartup, null); - - return; - } - - if (!FirstPoolUsageNeedsSniffing) - { - semaphore.Release(); - return; - } - - try - { - using (Audit(SniffOnStartup)) - { - Sniff(); - _nodePool.MarkAsSniffed(); - } - } - finally - { - semaphore.Release(); - } - } + public abstract Task SniffAsync(CancellationToken cancellationToken); - public async Task FirstPoolUsageAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken) - { - if (!FirstPoolUsageNeedsSniffing) return; - - // TODO cancellationToken could throw here and will bubble out as OperationCancelledException - // everywhere else it would bubble out wrapped in a `UnexpectedTransportException` - var success = await semaphore.WaitAsync(_settings.RequestTimeout, cancellationToken).ConfigureAwait(false); - if (!success) - { - if (FirstPoolUsageNeedsSniffing) - throw new PipelineException(PipelineFailure.CouldNotStartSniffOnStartup, null); - - return; - } - - if (!FirstPoolUsageNeedsSniffing) - { - semaphore.Release(); - return; - } - try - { - using (Audit(SniffOnStartup)) - { - await SniffAsync(cancellationToken).ConfigureAwait(false); - _nodePool.MarkAsSniffed(); - } - } - finally - { - semaphore.Release(); - } - } + public abstract void SniffOnStaleCluster(); - public void MarkAlive(Node node) => node.MarkAlive(); + public abstract Task SniffOnStaleClusterAsync(CancellationToken cancellationToken); - public void MarkDead(Node node) - { - var deadUntil = _dateTimeProvider.DeadTime(node.FailedAttempts, _settings.DeadTimeout, _settings.MaxDeadTimeout); - node.MarkDead(deadUntil); - Retried++; - } + public abstract void SniffOnConnectionFailure(); - /// - public bool TryGetSingleNode(out Node node) - { - if (_nodePool.Nodes.Count <= 1 && _nodePool.MaxRetries <= _nodePool.Nodes.Count && - !_nodePool.SupportsPinging && !_nodePool.SupportsReseeding) - { - node = _nodePool.Nodes.FirstOrDefault(); + public abstract Task SniffOnConnectionFailureAsync(CancellationToken cancellationToken); - if (node is not null && _nodePredicate(node)) return true; - } + public abstract void BadResponse(ref TResponse response, ApiCallDetails callDetails, RequestData data, TransportException exception) + where TResponse : TransportResponse, new(); - node = null; - return false; - } + public abstract void ThrowNoNodesAttempted(RequestData requestData, List seenExceptions); - public IEnumerable NextNode() - { - if (RequestConfiguration?.ForceNode != null) - { - yield return new Node(RequestConfiguration.ForceNode); - - yield break; - } - - //This for loop allows to break out of the view state machine if we need to - //force a refresh (after reseeding node pool). We have a hardcoded limit of only - //allowing 100 of these refreshes per call - var refreshed = false; - for (var i = 0; i < 100; i++) - { - if (DepletedRetries) yield break; - - foreach (var node in _nodePool.CreateView(LazyAuditable)) - { - if (DepletedRetries) break; - - if (!_nodePredicate(node)) continue; - - yield return node; - - if (!Refresh) continue; - - Refresh = false; - refreshed = true; - break; - } - //unless a refresh was requested we will not iterate over more then a single view. - //keep in mind refreshes are also still bound to overall maxretry count/timeout. - if (!refreshed) break; - } - } + public abstract void AuditCancellationRequested(); - public void Ping(Node node) - { - if (!_productRegistration.SupportsPing) return; - if (PingDisabled(node)) return; - - var pingData = _productRegistration.CreatePingRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); - using (var audit = Audit(PingSuccess, node)) - using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Ping, - pingData)) - { - audit.Path = pingData.PathAndQuery; - try - { - var response = _productRegistration.Ping(_transportClient, pingData); - d.EndState = response; - ThrowBadAuthPipelineExceptionWhenNeeded(response); - //ping should not silently accept bad but valid http responses - if (!response.Success) - throw new PipelineException(pingData.OnFailurePipelineFailure, response.OriginalException) { ApiCall = response }; - } - catch (Exception e) - { - var response = (e as PipelineException)?.ApiCall; - audit.Event = PingFailure; - audit.Exception = e; - throw new PipelineException(PipelineFailure.PingFailure, e) { ApiCall = response }; - } - } - } - - public async Task PingAsync(Node node, CancellationToken cancellationToken) - { - if (!_productRegistration.SupportsPing) return; - if (PingDisabled(node)) return; - - var pingData = _productRegistration.CreatePingRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); - using (var audit = Audit(PingSuccess, node)) - using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Ping, - pingData)) - { - audit.Path = pingData.PathAndQuery; - try - { - var apiCallDetails = await _productRegistration.PingAsync(_transportClient, pingData, cancellationToken).ConfigureAwait(false); - d.EndState = apiCallDetails; - ThrowBadAuthPipelineExceptionWhenNeeded(apiCallDetails); - //ping should not silently accept bad but valid http responses - if (!apiCallDetails.Success) - throw new PipelineException(pingData.OnFailurePipelineFailure, apiCallDetails.OriginalException) { ApiCall = apiCallDetails }; - } - catch (Exception e) - { - var response = (e as PipelineException)?.ApiCall; - audit.Event = PingFailure; - audit.Exception = e; - throw new PipelineException(PipelineFailure.PingFailure, e) { ApiCall = response }; - } - } - } - - public void Sniff() - { - if (!_productRegistration.SupportsSniff) return; - - var exceptions = new List(); - foreach (var node in SniffNodes) - { - var requestData = - _productRegistration.CreateSniffRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); - using (var audit = Audit(SniffSuccess, node)) - using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Sniff, - requestData)) - using (RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Sniff, requestData)) - try - { - audit.Path = requestData.PathAndQuery; - var (response, nodes) = _productRegistration.Sniff(_transportClient, _nodePool.UsingSsl, requestData); - d.EndState = response; - - ThrowBadAuthPipelineExceptionWhenNeeded(response); - //sniff should not silently accept bad but valid http responses - if (!response.Success) - throw new PipelineException(requestData.OnFailurePipelineFailure, response.OriginalException) { ApiCall = response }; - - _nodePool.Reseed(nodes); - Refresh = true; - return; - } - catch (Exception e) - { - audit.Event = SniffFailure; - audit.Exception = e; - exceptions.Add(e); - } - } - - throw new PipelineException(PipelineFailure.SniffFailure, exceptions.AsAggregateOrFirst()); - } - - public async Task SniffAsync(CancellationToken cancellationToken) - { - if (!_productRegistration.SupportsSniff) return; - - var exceptions = new List(); - foreach (var node in SniffNodes) - { - var requestData = - _productRegistration.CreateSniffRequestData(node, PingAndSniffRequestConfiguration, _settings, _memoryStreamFactory); - using (var audit = Audit(SniffSuccess, node)) - using (var d = RequestPipelineStatics.DiagnosticSource.Diagnose(DiagnosticSources.RequestPipeline.Sniff, - requestData)) - try - { - audit.Path = requestData.PathAndQuery; - var (response, nodes) = await _productRegistration - .SniffAsync(_transportClient, _nodePool.UsingSsl, requestData, cancellationToken) - .ConfigureAwait(false); - d.EndState = response; - - ThrowBadAuthPipelineExceptionWhenNeeded(response); - //sniff should not silently accept bad but valid http responses - if (!response.Success) - throw new PipelineException(requestData.OnFailurePipelineFailure, response.OriginalException) { ApiCall = response }; - - _nodePool.Reseed(nodes); - Refresh = true; - return; - } - catch (Exception e) - { - audit.Event = SniffFailure; - audit.Exception = e; - exceptions.Add(e); - } - } - - throw new PipelineException(PipelineFailure.SniffFailure, exceptions.AsAggregateOrFirst()); - } - - public void SniffOnConnectionFailure() - { - if (!SniffsOnConnectionFailure) return; - - using (Audit(SniffOnFail)) - Sniff(); - } - - public async Task SniffOnConnectionFailureAsync(CancellationToken cancellationToken) - { - if (!SniffsOnConnectionFailure) return; - - using (Audit(SniffOnFail)) - await SniffAsync(cancellationToken).ConfigureAwait(false); - } - - public void SniffOnStaleCluster() - { - if (!StaleClusterState) return; - - using (Audit(AuditEvent.SniffOnStaleCluster)) - { - Sniff(); - _nodePool.MarkAsSniffed(); - } - } - - public async Task SniffOnStaleClusterAsync(CancellationToken cancellationToken) - { - if (!StaleClusterState) return; - - using (Audit(AuditEvent.SniffOnStaleCluster)) - { - await SniffAsync(cancellationToken).ConfigureAwait(false); - _nodePool.MarkAsSniffed(); - } - } - - public void ThrowNoNodesAttempted(RequestData requestData, List seenExceptions) - { - var clientException = new TransportException(PipelineFailure.NoNodesAttempted, RequestPipelineStatics.NoNodesAttemptedMessage, - (Exception)null); - using (Audit(NoNodesAttempted)) - throw new UnexpectedTransportException(clientException, seenExceptions) { Request = requestData, AuditTrail = AuditTrail }; - } - - private bool PingDisabled(Node node) => - (RequestConfiguration?.DisablePing).GetValueOrDefault(false) - || _settings.DisablePings || !_nodePool.SupportsPinging || !node.IsResurrected; + public abstract TransportException CreateClientException(TResponse response, ApiCallDetails callDetails, RequestData data, + List seenExceptions) + where TResponse : TransportResponse, new(); +#pragma warning restore 1591 - private Auditable Audit(AuditEvent type, Node node = null) => new(type, AuditTrail, _dateTimeProvider, node); + /// + /// + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } - private static void ThrowBadAuthPipelineExceptionWhenNeeded(IApiCallDetails details, ITransportResponse response = null) - { - if (details?.HttpStatusCode == 401) - throw new PipelineException(PipelineFailure.BadAuthentication, details.OriginalException) { Response = response, ApiCall = details }; - } + /// + /// + /// + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; - private void LazyAuditable(AuditEvent e, Node n) + if (disposing) { - using (new Auditable(e, AuditTrail, _dateTimeProvider, n)) { } + DisposeManagedResources(); } - protected virtual void Dispose() { } + _disposed = true; } -#pragma warning restore 1591 + + /// + /// + /// + protected virtual void DisposeManagedResources() { } } diff --git a/src/Elastic.Transport/Components/Pipeline/RequestPipelineStatics.cs b/src/Elastic.Transport/Components/Pipeline/RequestPipelineStatics.cs new file mode 100644 index 0000000..a009e8e --- /dev/null +++ b/src/Elastic.Transport/Components/Pipeline/RequestPipelineStatics.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics; +using Elastic.Transport.Diagnostics; + +//#if NETSTANDARD2_0 || NETSTANDARD2_1 +//using System.Threading.Tasks.Extensions; +//#endif + +namespace Elastic.Transport; + +internal static class RequestPipelineStatics +{ + public static readonly string NoNodesAttemptedMessage = + "No nodes were attempted, this can happen when a node predicate does not match any nodes"; + + public static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.RequestPipeline.SourceName); +} +#pragma warning restore 1591 diff --git a/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs b/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs index 29b611c..132a043 100644 --- a/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs +++ b/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs @@ -10,43 +10,42 @@ using System.Threading.Tasks; using Elastic.Transport.Diagnostics; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Builds a from the provided response data. +/// +public abstract class ResponseBuilder { /// - /// Builds a from the provided response data. + /// Create an instance of from /// - public abstract class ResponseBuilder - { - /// - /// Create an instance of from - /// - public abstract TResponse ToResponse( - RequestData requestData, - Exception ex, - int? statusCode, - Dictionary> headers, - Stream responseStream, - string mimeType, - long contentLength, - IReadOnlyDictionary threadPoolStats, - IReadOnlyDictionary tcpStats + public abstract TResponse ToResponse( + RequestData requestData, + Exception ex, + int? statusCode, + Dictionary> headers, + Stream responseStream, + string mimeType, + long contentLength, + IReadOnlyDictionary threadPoolStats, + IReadOnlyDictionary tcpStats - ) where TResponse : class, ITransportResponse, new(); + ) where TResponse : TransportResponse, new(); - /// - /// Create an instance of from - /// - public abstract Task ToResponseAsync( - RequestData requestData, - Exception ex, - int? statusCode, - Dictionary> headers, - Stream responseStream, - string mimeType, - long contentLength, - IReadOnlyDictionary threadPoolStats, - IReadOnlyDictionary tcpStats, - CancellationToken cancellationToken = default - ) where TResponse : class, ITransportResponse, new(); - } + /// + /// Create an instance of from + /// + public abstract Task ToResponseAsync( + RequestData requestData, + Exception ex, + int? statusCode, + Dictionary> headers, + Stream responseStream, + string mimeType, + long contentLength, + IReadOnlyDictionary threadPoolStats, + IReadOnlyDictionary tcpStats, + CancellationToken cancellationToken = default + ) where TResponse : TransportResponse, new(); } diff --git a/src/Elastic.Transport/Components/Providers/DateTimeProvider.cs b/src/Elastic.Transport/Components/Providers/DateTimeProvider.cs index bad05e5..283973b 100644 --- a/src/Elastic.Transport/Components/Providers/DateTimeProvider.cs +++ b/src/Elastic.Transport/Components/Providers/DateTimeProvider.cs @@ -4,26 +4,24 @@ using System; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// An abstraction to provide access to the current . This abstraction allows time to be tested within +/// the transport. +/// +public abstract class DateTimeProvider { - /// - public class DateTimeProvider : IDateTimeProvider - { - /// A static instance to reuse as is stateless - public static readonly DateTimeProvider Default = new(); - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); - private static readonly TimeSpan MaximumTimeout = TimeSpan.FromMinutes(30); + internal DateTimeProvider() { } - /// - public virtual DateTime DeadTime(int attempts, TimeSpan? minDeadTimeout, TimeSpan? maxDeadTimeout) - { - var timeout = minDeadTimeout.GetValueOrDefault(DefaultTimeout); - var maxTimeout = maxDeadTimeout.GetValueOrDefault(MaximumTimeout); - var milliSeconds = Math.Min(timeout.TotalMilliseconds * 2 * Math.Pow(2, attempts * 0.5 - 1), maxTimeout.TotalMilliseconds); - return Now().AddMilliseconds(milliSeconds); - } + /// The current date time + public abstract DateTime Now(); - /// - public virtual DateTime Now() => DateTime.UtcNow; - } + /// + /// Calculate the dead time for a node based on the number of attempts. + /// + /// The number of attempts on the node + /// The initial dead time as configured by + /// The configured maximum dead timeout as configured by + public abstract DateTime DeadTime(int attempts, TimeSpan? minDeadTimeout, TimeSpan? maxDeadTimeout); } diff --git a/src/Elastic.Transport/Components/Providers/DefaultDateTimeProvider.cs b/src/Elastic.Transport/Components/Providers/DefaultDateTimeProvider.cs new file mode 100644 index 0000000..fbe4c9f --- /dev/null +++ b/src/Elastic.Transport/Components/Providers/DefaultDateTimeProvider.cs @@ -0,0 +1,28 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; + +namespace Elastic.Transport; + +/// +public sealed class DefaultDateTimeProvider : DateTimeProvider +{ + /// A static instance to reuse as is stateless + public static readonly DefaultDateTimeProvider Default = new(); + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan MaximumTimeout = TimeSpan.FromMinutes(30); + + /// + public override DateTime DeadTime(int attempts, TimeSpan? minDeadTimeout, TimeSpan? maxDeadTimeout) + { + var timeout = minDeadTimeout.GetValueOrDefault(DefaultTimeout); + var maxTimeout = maxDeadTimeout.GetValueOrDefault(MaximumTimeout); + var milliSeconds = Math.Min(timeout.TotalMilliseconds * 2 * Math.Pow(2, attempts * 0.5 - 1), maxTimeout.TotalMilliseconds); + return Now().AddMilliseconds(milliSeconds); + } + + /// + public override DateTime Now() => DateTime.UtcNow; +} diff --git a/src/Elastic.Transport/Components/Providers/DefaultMemoryStreamFactory.cs b/src/Elastic.Transport/Components/Providers/DefaultMemoryStreamFactory.cs new file mode 100644 index 0000000..be19af4 --- /dev/null +++ b/src/Elastic.Transport/Components/Providers/DefaultMemoryStreamFactory.cs @@ -0,0 +1,25 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO; + +namespace Elastic.Transport; + +/// +/// A factory for creating memory streams using instances of +/// +public sealed class DefaultMemoryStreamFactory : MemoryStreamFactory +{ + /// Provide a static instance of this stateless class, so it can be reused + public static DefaultMemoryStreamFactory Default { get; } = new DefaultMemoryStreamFactory(); + + /// + public override MemoryStream Create() => new(); + + /// + public override MemoryStream Create(byte[] bytes) => new(bytes); + + /// + public override MemoryStream Create(byte[] bytes, int index, int count) => new(bytes, index, count); +} diff --git a/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs b/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs new file mode 100644 index 0000000..72df8a0 --- /dev/null +++ b/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs @@ -0,0 +1,19 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Transport; + +/// +/// The default implementation for that returns +/// +internal sealed class DefaultRequestPipelineFactory : RequestPipelineFactory + where TConfiguration : class, ITransportConfiguration +{ + /// + /// returns instances of + /// + public override RequestPipeline Create(TConfiguration configurationValues, DateTimeProvider dateTimeProvider, + MemoryStreamFactory memoryStreamFactory, RequestParameters requestParameters) => + new DefaultRequestPipeline(configurationValues, dateTimeProvider, memoryStreamFactory, requestParameters); +} diff --git a/src/Elastic.Transport/Components/Providers/IDateTimeProvider.cs b/src/Elastic.Transport/Components/Providers/IDateTimeProvider.cs deleted file mode 100644 index f1d0c62..0000000 --- a/src/Elastic.Transport/Components/Providers/IDateTimeProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; - -namespace Elastic.Transport -{ - /// - /// An abstraction to provide access to the current . This abstraction allows time to be tested within - /// the transport. - /// - public interface IDateTimeProvider - { - /// The current date time - DateTime Now(); - - /// - /// Calculate the dead time for a node based on the number of attempts. - /// - /// The number of attempts on the node - /// The initial dead time as configured by - /// The configured maximum dead timeout as configured by - DateTime DeadTime(int attempts, TimeSpan? minDeadTimeout, TimeSpan? maxDeadTimeout); - } -} diff --git a/src/Elastic.Transport/Components/Providers/IMemoryStreamFactory.cs b/src/Elastic.Transport/Components/Providers/IMemoryStreamFactory.cs deleted file mode 100644 index b6c71f2..0000000 --- a/src/Elastic.Transport/Components/Providers/IMemoryStreamFactory.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.IO; - -namespace Elastic.Transport -{ - /// - /// A factory for creating memory streams - /// - public interface IMemoryStreamFactory - { - /// - /// Creates a memory stream - /// - MemoryStream Create(); - - /// - /// Creates a memory stream with the bytes written to the stream - /// - MemoryStream Create(byte[] bytes); - - /// - /// Creates a memory stream with the bytes written to the stream - /// - MemoryStream Create(byte[] bytes, int index, int count); - } -} diff --git a/src/Elastic.Transport/Components/Providers/IRequestPipelineFactory.cs b/src/Elastic.Transport/Components/Providers/IRequestPipelineFactory.cs deleted file mode 100644 index 9ea2b3a..0000000 --- a/src/Elastic.Transport/Components/Providers/IRequestPipelineFactory.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.Transport -{ - /// A factory that creates instances of , this factory exists so that transport can be tested. - public interface IRequestPipelineFactory - where TConfiguration : class, ITransportConfiguration - { - /// Create an instance of - IRequestPipeline Create(TConfiguration configuration, IDateTimeProvider dateTimeProvider, - IMemoryStreamFactory memoryStreamFactory, IRequestParameters requestParameters - ); - } - - /// - /// The default implementation for that returns - /// - internal class RequestPipelineFactory : IRequestPipelineFactory - where TConfiguration : class, ITransportConfiguration - { - /// - /// returns instances of - /// - public IRequestPipeline Create(TConfiguration configurationValues, IDateTimeProvider dateTimeProvider, - IMemoryStreamFactory memoryStreamFactory, IRequestParameters requestParameters - ) => - new RequestPipeline(configurationValues, dateTimeProvider, memoryStreamFactory, requestParameters); - - } -} diff --git a/src/Elastic.Transport/Components/Providers/MemoryStreamFactory.cs b/src/Elastic.Transport/Components/Providers/MemoryStreamFactory.cs index 58b1674..c1f5785 100644 --- a/src/Elastic.Transport/Components/Providers/MemoryStreamFactory.cs +++ b/src/Elastic.Transport/Components/Providers/MemoryStreamFactory.cs @@ -4,23 +4,27 @@ using System.IO; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A factory for creating memory streams +/// +public abstract class MemoryStreamFactory { + internal MemoryStreamFactory() { } + /// - /// A factory for creating memory streams using instances of + /// Creates a memory stream /// - public sealed class MemoryStreamFactory : IMemoryStreamFactory - { - /// Provide a static instance of this stateless class, so it can be reused - public static MemoryStreamFactory Default { get; } = new MemoryStreamFactory(); + public abstract MemoryStream Create(); - /// - public MemoryStream Create() => new(); - - /// - public MemoryStream Create(byte[] bytes) => new(bytes); + /// + /// Creates a memory stream with the bytes written to the stream + /// + public abstract MemoryStream Create(byte[] bytes); - /// - public MemoryStream Create(byte[] bytes, int index, int count) => new(bytes, index, count); - } + /// + /// Creates a memory stream with the bytes written to the stream + /// + public abstract MemoryStream Create(byte[] bytes, int index, int count); } diff --git a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStream.cs b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStream.cs index a169215..9f17cb3 100644 --- a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStream.cs +++ b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStream.cs @@ -33,510 +33,510 @@ using System.IO; using System.Threading; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// MemoryStream implementation that deals with pooling and managing memory streams which use potentially large +/// buffers. +/// +/// +/// This class works in tandem with the RecyclableMemoryStreamManager to supply MemoryStream +/// objects to callers, while avoiding these specific problems: +/// 1. LOH allocations - since all large buffers are pooled, they will never incur a Gen2 GC +/// 2. Memory waste - A standard memory stream doubles its size when it runs out of room. This +/// leads to continual memory growth as each stream approaches the maximum allowed size. +/// 3. Memory copying - Each time a MemoryStream grows, all the bytes are copied into new buffers. +/// This implementation only copies the bytes when GetBuffer is called. +/// 4. Memory fragmentation - By using homogeneous buffer sizes, it ensures that blocks of memory +/// can be easily reused. +/// The stream is implemented on top of a series of uniformly-sized blocks. As the stream's length grows, +/// additional blocks are retrieved from the memory manager. It is these blocks that are pooled, not the stream +/// object itself. +/// The biggest wrinkle in this implementation is when GetBuffer() is called. This requires a single +/// contiguous buffer. If only a single block is in use, then that block is returned. If multiple blocks +/// are in use, we retrieve a larger buffer from the memory manager. These large buffers are also pooled, +/// split by size--they are multiples/exponentials of a chunk size (1 MB by default). +/// Once a large buffer is assigned to the stream the blocks are NEVER again used for this stream. All operations take +/// place on the +/// large buffer. The large buffer can be replaced by a larger buffer from the pool as needed. All blocks and large buffers +/// are maintained in the stream until the stream is disposed (unless AggressiveBufferReturn is enabled in the stream +/// manager). +/// +internal sealed class RecyclableMemoryStream : MemoryStream { + private const long MaxStreamLength = int.MaxValue; + + private static readonly byte[] EmptyArray = new byte[0]; + + /// + /// All of these blocks must be the same size + /// + private readonly List _blocks = new List(1); + /// - /// MemoryStream implementation that deals with pooling and managing memory streams which use potentially large - /// buffers. + /// This buffer exists so that WriteByte can forward all of its calls to Write + /// without creating a new byte[] buffer on every call. + /// + private readonly byte[] _byteBuffer = new byte[1]; + + private readonly Guid _id; + + private readonly RecyclableMemoryStreamManager _memoryManager; + + private readonly string _tag; + + /// + /// This list is used to store buffers once they're replaced by something larger. + /// This is for the cases where you have users of this class that may hold onto the buffers longer + /// than they should and you want to prevent race conditions which could corrupt the data. + /// + private List _dirtyBuffers; + + // long to allow Interlocked.Read (for .NET Standard 1.4 compat) + private long _disposedState; + + /// + /// This is only set by GetBuffer() if the necessary buffer is larger than a single block size, or on + /// construction if the caller immediately requests a single large buffer. /// /// - /// This class works in tandem with the RecyclableMemoryStreamManager to supply MemoryStream - /// objects to callers, while avoiding these specific problems: - /// 1. LOH allocations - since all large buffers are pooled, they will never incur a Gen2 GC - /// 2. Memory waste - A standard memory stream doubles its size when it runs out of room. This - /// leads to continual memory growth as each stream approaches the maximum allowed size. - /// 3. Memory copying - Each time a MemoryStream grows, all the bytes are copied into new buffers. - /// This implementation only copies the bytes when GetBuffer is called. - /// 4. Memory fragmentation - By using homogeneous buffer sizes, it ensures that blocks of memory - /// can be easily reused. - /// The stream is implemented on top of a series of uniformly-sized blocks. As the stream's length grows, - /// additional blocks are retrieved from the memory manager. It is these blocks that are pooled, not the stream - /// object itself. - /// The biggest wrinkle in this implementation is when GetBuffer() is called. This requires a single - /// contiguous buffer. If only a single block is in use, then that block is returned. If multiple blocks - /// are in use, we retrieve a larger buffer from the memory manager. These large buffers are also pooled, - /// split by size--they are multiples/exponentials of a chunk size (1 MB by default). - /// Once a large buffer is assigned to the stream the blocks are NEVER again used for this stream. All operations take - /// place on the - /// large buffer. The large buffer can be replaced by a larger buffer from the pool as needed. All blocks and large buffers - /// are maintained in the stream until the stream is disposed (unless AggressiveBufferReturn is enabled in the stream - /// manager). + /// If this field is non-null, it contains the concatenation of the bytes found in the individual + /// blocks. Once it is created, this (or a larger) largeBuffer will be used for the life of the stream. /// - internal sealed class RecyclableMemoryStream : MemoryStream + private byte[] _largeBuffer; + + /// + /// Unique identifier for this stream across it's entire lifetime + /// + /// Object has been disposed + internal Guid Id { - private const long MaxStreamLength = int.MaxValue; - - private static readonly byte[] EmptyArray = new byte[0]; - - /// - /// All of these blocks must be the same size - /// - private readonly List _blocks = new List(1); - - /// - /// This buffer exists so that WriteByte can forward all of its calls to Write - /// without creating a new byte[] buffer on every call. - /// - private readonly byte[] _byteBuffer = new byte[1]; - - private readonly Guid _id; - - private readonly RecyclableMemoryStreamManager _memoryManager; - - private readonly string _tag; - - /// - /// This list is used to store buffers once they're replaced by something larger. - /// This is for the cases where you have users of this class that may hold onto the buffers longer - /// than they should and you want to prevent race conditions which could corrupt the data. - /// - private List _dirtyBuffers; - - // long to allow Interlocked.Read (for .NET Standard 1.4 compat) - private long _disposedState; - - /// - /// This is only set by GetBuffer() if the necessary buffer is larger than a single block size, or on - /// construction if the caller immediately requests a single large buffer. - /// - /// - /// If this field is non-null, it contains the concatenation of the bytes found in the individual - /// blocks. Once it is created, this (or a larger) largeBuffer will be used for the life of the stream. - /// - private byte[] _largeBuffer; - - /// - /// Unique identifier for this stream across it's entire lifetime - /// - /// Object has been disposed - internal Guid Id + get { - get - { - CheckDisposed(); - return _id; - } + CheckDisposed(); + return _id; } + } - /// - /// A temporary identifier for the current usage of this stream. - /// - /// Object has been disposed - internal string Tag + /// + /// A temporary identifier for the current usage of this stream. + /// + /// Object has been disposed + internal string Tag + { + get { - get - { - CheckDisposed(); - return _tag; - } + CheckDisposed(); + return _tag; } + } - /// - /// Gets the memory manager being used by this stream. - /// - /// Object has been disposed - internal RecyclableMemoryStreamManager MemoryManager + /// + /// Gets the memory manager being used by this stream. + /// + /// Object has been disposed + internal RecyclableMemoryStreamManager MemoryManager + { + get { - get - { - CheckDisposed(); - return _memoryManager; - } + CheckDisposed(); + return _memoryManager; } + } - /// - /// Callstack of the constructor. It is only set if MemoryManager.GenerateCallStacks is true, - /// which should only be in debugging situations. - /// - internal string AllocationStack { get; } - - /// - /// Callstack of the Dispose call. It is only set if MemoryManager.GenerateCallStacks is true, - /// which should only be in debugging situations. - /// - internal string DisposeStack { get; private set; } - - #region Constructors - - /// - /// Allocate a new RecyclableMemoryStream object. - /// - /// The memory manager - public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager) - : this(memoryManager, Guid.NewGuid(), null, 0, null) { } - - /// - /// Allocate a new RecyclableMemoryStream object. - /// - /// The memory manager - /// A unique identifier which can be used to trace usages of the stream. - public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id) - : this(memoryManager, id, null, 0, null) { } - - /// - /// Allocate a new RecyclableMemoryStream object - /// - /// The memory manager - /// A string identifying this stream for logging and debugging purposes - public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, string tag) - : this(memoryManager, Guid.NewGuid(), tag, 0, null) { } - - /// - /// Allocate a new RecyclableMemoryStream object - /// - /// The memory manager - /// A unique identifier which can be used to trace usages of the stream. - /// A string identifying this stream for logging and debugging purposes - public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id, string tag) - : this(memoryManager, id, tag, 0, null) { } - - /// - /// Allocate a new RecyclableMemoryStream object - /// - /// The memory manager - /// A string identifying this stream for logging and debugging purposes - /// The initial requested size to prevent future allocations - public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, string tag, int requestedSize) - : this(memoryManager, Guid.NewGuid(), tag, requestedSize, null) { } - - /// - /// Allocate a new RecyclableMemoryStream object - /// - /// The memory manager - /// A unique identifier which can be used to trace usages of the stream. - /// A string identifying this stream for logging and debugging purposes - /// The initial requested size to prevent future allocations - public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id, string tag, int requestedSize) - : this(memoryManager, id, tag, requestedSize, null) { } - - /// - /// Allocate a new RecyclableMemoryStream object - /// - /// The memory manager - /// A unique identifier which can be used to trace usages of the stream. - /// A string identifying this stream for logging and debugging purposes - /// The initial requested size to prevent future allocations - /// - /// An initial buffer to use. This buffer will be owned by the stream and returned to the - /// memory manager upon Dispose. - /// - internal RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id, string tag, int requestedSize, byte[] initialLargeBuffer - ) - : base(EmptyArray) - { - _memoryManager = memoryManager; - _id = id; - _tag = tag; + /// + /// Callstack of the constructor. It is only set if MemoryManager.GenerateCallStacks is true, + /// which should only be in debugging situations. + /// + internal string AllocationStack { get; } - if (requestedSize < memoryManager.BlockSize) requestedSize = memoryManager.BlockSize; + /// + /// Callstack of the Dispose call. It is only set if MemoryManager.GenerateCallStacks is true, + /// which should only be in debugging situations. + /// + internal string DisposeStack { get; private set; } - if (initialLargeBuffer == null) - EnsureCapacity(requestedSize); - else - _largeBuffer = initialLargeBuffer; + #region Constructors - if (_memoryManager.GenerateCallStacks) AllocationStack = Environment.StackTrace; + /// + /// Allocate a new RecyclableMemoryStream object. + /// + /// The memory manager + public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager) + : this(memoryManager, Guid.NewGuid(), null, 0, null) { } - RecyclableMemoryStreamManager.EventsWriter.MemoryStreamCreated(_id, _tag, requestedSize); - _memoryManager.ReportStreamCreated(); - } + /// + /// Allocate a new RecyclableMemoryStream object. + /// + /// The memory manager + /// A unique identifier which can be used to trace usages of the stream. + public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id) + : this(memoryManager, id, null, 0, null) { } - #endregion + /// + /// Allocate a new RecyclableMemoryStream object + /// + /// The memory manager + /// A string identifying this stream for logging and debugging purposes + public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, string tag) + : this(memoryManager, Guid.NewGuid(), tag, 0, null) { } - #region Dispose and Finalize + /// + /// Allocate a new RecyclableMemoryStream object + /// + /// The memory manager + /// A unique identifier which can be used to trace usages of the stream. + /// A string identifying this stream for logging and debugging purposes + public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id, string tag) + : this(memoryManager, id, tag, 0, null) { } - ~RecyclableMemoryStream() => Dispose(false); + /// + /// Allocate a new RecyclableMemoryStream object + /// + /// The memory manager + /// A string identifying this stream for logging and debugging purposes + /// The initial requested size to prevent future allocations + public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, string tag, int requestedSize) + : this(memoryManager, Guid.NewGuid(), tag, requestedSize, null) { } - /// - /// Returns the memory used by this stream back to the pool. - /// - /// Whether we're disposing (true), or being called by the finalizer (false) - [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly", - Justification = "We have different disposal semantics, so SuppressFinalize is in a different spot.")] - protected override void Dispose(bool disposing) - { - if (Interlocked.CompareExchange(ref _disposedState, 1, 0) != 0) - { - string doubleDisposeStack = null; - if (_memoryManager.GenerateCallStacks) doubleDisposeStack = Environment.StackTrace; + /// + /// Allocate a new RecyclableMemoryStream object + /// + /// The memory manager + /// A unique identifier which can be used to trace usages of the stream. + /// A string identifying this stream for logging and debugging purposes + /// The initial requested size to prevent future allocations + public RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id, string tag, int requestedSize) + : this(memoryManager, id, tag, requestedSize, null) { } - RecyclableMemoryStreamManager.EventsWriter.MemoryStreamDoubleDispose(_id, _tag, - AllocationStack, - DisposeStack, - doubleDisposeStack); - return; - } + /// + /// Allocate a new RecyclableMemoryStream object + /// + /// The memory manager + /// A unique identifier which can be used to trace usages of the stream. + /// A string identifying this stream for logging and debugging purposes + /// The initial requested size to prevent future allocations + /// + /// An initial buffer to use. This buffer will be owned by the stream and returned to the + /// memory manager upon Dispose. + /// + internal RecyclableMemoryStream(RecyclableMemoryStreamManager memoryManager, Guid id, string tag, int requestedSize, byte[] initialLargeBuffer + ) + : base(EmptyArray) + { + _memoryManager = memoryManager; + _id = id; + _tag = tag; - RecyclableMemoryStreamManager.EventsWriter.MemoryStreamDisposed(_id, _tag); + if (requestedSize < memoryManager.BlockSize) requestedSize = memoryManager.BlockSize; - if (_memoryManager.GenerateCallStacks) DisposeStack = Environment.StackTrace; + if (initialLargeBuffer == null) + EnsureCapacity(requestedSize); + else + _largeBuffer = initialLargeBuffer; - if (disposing) - { - _memoryManager.ReportStreamDisposed(); + if (_memoryManager.GenerateCallStacks) AllocationStack = Environment.StackTrace; - GC.SuppressFinalize(this); - } - else - { - // We're being finalized. + RecyclableMemoryStreamManager.EventsWriter.MemoryStreamCreated(_id, _tag, requestedSize); + _memoryManager.ReportStreamCreated(); + } + + #endregion + + #region Dispose and Finalize - RecyclableMemoryStreamManager.EventsWriter.MemoryStreamFinalized(_id, _tag, AllocationStack); + ~RecyclableMemoryStream() => Dispose(false); + + /// + /// Returns the memory used by this stream back to the pool. + /// + /// Whether we're disposing (true), or being called by the finalizer (false) + [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly", + Justification = "We have different disposal semantics, so SuppressFinalize is in a different spot.")] + protected override void Dispose(bool disposing) + { + if (Interlocked.CompareExchange(ref _disposedState, 1, 0) != 0) + { + string doubleDisposeStack = null; + if (_memoryManager.GenerateCallStacks) doubleDisposeStack = Environment.StackTrace; + + RecyclableMemoryStreamManager.EventsWriter.MemoryStreamDoubleDispose(_id, _tag, + AllocationStack, + DisposeStack, + doubleDisposeStack); + return; + } + + RecyclableMemoryStreamManager.EventsWriter.MemoryStreamDisposed(_id, _tag); + + if (_memoryManager.GenerateCallStacks) DisposeStack = Environment.StackTrace; + + if (disposing) + { + _memoryManager.ReportStreamDisposed(); + + GC.SuppressFinalize(this); + } + else + { + // We're being finalized. + + RecyclableMemoryStreamManager.EventsWriter.MemoryStreamFinalized(_id, _tag, AllocationStack); #if !NETSTANDARD1_4 - if (AppDomain.CurrentDomain.IsFinalizingForUnload()) - { - // If we're being finalized because of a shutdown, don't go any further. - // We have no idea what's already been cleaned up. Triggering events may cause - // a crash. - base.Dispose(false); - return; - } + if (AppDomain.CurrentDomain.IsFinalizingForUnload()) + { + // If we're being finalized because of a shutdown, don't go any further. + // We have no idea what's already been cleaned up. Triggering events may cause + // a crash. + base.Dispose(false); + return; + } #endif - _memoryManager.ReportStreamFinalized(); - } + _memoryManager.ReportStreamFinalized(); + } - _memoryManager.ReportStreamLength(_length); + _memoryManager.ReportStreamLength(_length); - if (_largeBuffer != null) _memoryManager.ReturnLargeBuffer(_largeBuffer, _tag); + if (_largeBuffer != null) _memoryManager.ReturnLargeBuffer(_largeBuffer, _tag); - if (_dirtyBuffers != null) - foreach (var buffer in _dirtyBuffers) - _memoryManager.ReturnLargeBuffer(buffer, _tag); + if (_dirtyBuffers != null) + foreach (var buffer in _dirtyBuffers) + _memoryManager.ReturnLargeBuffer(buffer, _tag); - _memoryManager.ReturnBlocks(_blocks, _tag); - _blocks.Clear(); + _memoryManager.ReturnBlocks(_blocks, _tag); + _blocks.Clear(); - base.Dispose(disposing); - } + base.Dispose(disposing); + } - /// - /// Equivalent to Dispose - /// + /// + /// Equivalent to Dispose + /// #if NETSTANDARD1_4 public void Close() => Dispose(true); #else - public override void Close() => Dispose(true); + public override void Close() => Dispose(true); #endif #endregion - #region MemoryStream overrides - - /// - /// Gets or sets the capacity - /// - /// - /// Capacity is always in multiples of the memory manager's block size, unless - /// the large buffer is in use. Capacity never decreases during a stream's lifetime. - /// Explicitly setting the capacity to a lower value than the current value will have no effect. - /// This is because the buffers are all pooled by chunks and there's little reason to - /// allow stream truncation. - /// - /// Object has been disposed - public override int Capacity + #region MemoryStream overrides + + /// + /// Gets or sets the capacity + /// + /// + /// Capacity is always in multiples of the memory manager's block size, unless + /// the large buffer is in use. Capacity never decreases during a stream's lifetime. + /// Explicitly setting the capacity to a lower value than the current value will have no effect. + /// This is because the buffers are all pooled by chunks and there's little reason to + /// allow stream truncation. + /// + /// Object has been disposed + public override int Capacity + { + get { - get - { - CheckDisposed(); - if (_largeBuffer != null) return _largeBuffer.Length; + CheckDisposed(); + if (_largeBuffer != null) return _largeBuffer.Length; - var size = (long)_blocks.Count * _memoryManager.BlockSize; - return (int)Math.Min(int.MaxValue, size); - } - set - { - CheckDisposed(); - EnsureCapacity(value); - } + var size = (long)_blocks.Count * _memoryManager.BlockSize; + return (int)Math.Min(int.MaxValue, size); + } + set + { + CheckDisposed(); + EnsureCapacity(value); } + } - private int _length; + private int _length; - /// - /// Gets the number of bytes written to this stream. - /// - /// Object has been disposed - public override long Length + /// + /// Gets the number of bytes written to this stream. + /// + /// Object has been disposed + public override long Length + { + get { - get - { - CheckDisposed(); - return _length; - } + CheckDisposed(); + return _length; } + } - private int _position; + private int _position; - /// - /// Gets the current position in the stream - /// - /// Object has been disposed - public override long Position + /// + /// Gets the current position in the stream + /// + /// Object has been disposed + public override long Position + { + get { - get - { - CheckDisposed(); - return _position; - } - set - { - CheckDisposed(); - if (value < 0) throw new ArgumentOutOfRangeException("value", "value must be non-negative"); + CheckDisposed(); + return _position; + } + set + { + CheckDisposed(); + if (value < 0) throw new ArgumentOutOfRangeException("value", "value must be non-negative"); - if (value > MaxStreamLength) throw new ArgumentOutOfRangeException("value", "value cannot be more than " + MaxStreamLength); + if (value > MaxStreamLength) throw new ArgumentOutOfRangeException("value", "value cannot be more than " + MaxStreamLength); - _position = (int)value; - } + _position = (int)value; } + } + + /// + /// Whether the stream can currently read + /// + public override bool CanRead => !Disposed; + + /// + /// Whether the stream can currently seek + /// + public override bool CanSeek => !Disposed; + + /// + /// Always false + /// + public override bool CanTimeout => false; + + /// + /// Whether the stream can currently write + /// + public override bool CanWrite => !Disposed; - /// - /// Whether the stream can currently read - /// - public override bool CanRead => !Disposed; - - /// - /// Whether the stream can currently seek - /// - public override bool CanSeek => !Disposed; - - /// - /// Always false - /// - public override bool CanTimeout => false; - - /// - /// Whether the stream can currently write - /// - public override bool CanWrite => !Disposed; - - /// - /// Returns a single buffer containing the contents of the stream. - /// The buffer may be longer than the stream length. - /// - /// A byte[] buffer - /// - /// IMPORTANT: Doing a Write() after calling GetBuffer() invalidates the buffer. The old buffer is held onto - /// until Dispose is called, but the next time GetBuffer() is called, a new buffer from the pool will be required. - /// - /// Object has been disposed + /// + /// Returns a single buffer containing the contents of the stream. + /// The buffer may be longer than the stream length. + /// + /// A byte[] buffer + /// + /// IMPORTANT: Doing a Write() after calling GetBuffer() invalidates the buffer. The old buffer is held onto + /// until Dispose is called, but the next time GetBuffer() is called, a new buffer from the pool will be required. + /// + /// Object has been disposed #if NETSTANDARD1_4 public byte[] GetBuffer() #else - public override byte[] GetBuffer() + public override byte[] GetBuffer() #endif - { - CheckDisposed(); - - if (_largeBuffer != null) return _largeBuffer; + { + CheckDisposed(); - if (_blocks.Count == 1) return _blocks[0]; + if (_largeBuffer != null) return _largeBuffer; - // Buffer needs to reflect the capacity, not the length, because - // it's possible that people will manipulate the buffer directly - // and set the length afterward. Capacity sets the expectation - // for the size of the buffer. - var newBuffer = _memoryManager.GetLargeBuffer(Capacity, _tag); + if (_blocks.Count == 1) return _blocks[0]; - // InternalRead will check for existence of largeBuffer, so make sure we - // don't set it until after we've copied the data. - InternalRead(newBuffer, 0, _length, 0); - _largeBuffer = newBuffer; + // Buffer needs to reflect the capacity, not the length, because + // it's possible that people will manipulate the buffer directly + // and set the length afterward. Capacity sets the expectation + // for the size of the buffer. + var newBuffer = _memoryManager.GetLargeBuffer(Capacity, _tag); - if (_blocks.Count > 0 && _memoryManager.AggressiveBufferReturn) - { - _memoryManager.ReturnBlocks(_blocks, _tag); - _blocks.Clear(); - } + // InternalRead will check for existence of largeBuffer, so make sure we + // don't set it until after we've copied the data. + InternalRead(newBuffer, 0, _length, 0); + _largeBuffer = newBuffer; - return _largeBuffer; + if (_blocks.Count > 0 && _memoryManager.AggressiveBufferReturn) + { + _memoryManager.ReturnBlocks(_blocks, _tag); + _blocks.Clear(); } - /// - /// Returns an ArraySegment that wraps a single buffer containing the contents of the stream. - /// - /// An ArraySegment containing a reference to the underlying bytes. - /// Always returns true. - /// - /// GetBuffer has no failure modes (it always returns something, even if it's an empty buffer), therefore this method - /// always returns a valid ArraySegment to the same buffer returned by GetBuffer. - /// + return _largeBuffer; + } + + /// + /// Returns an ArraySegment that wraps a single buffer containing the contents of the stream. + /// + /// An ArraySegment containing a reference to the underlying bytes. + /// Always returns true. + /// + /// GetBuffer has no failure modes (it always returns something, even if it's an empty buffer), therefore this method + /// always returns a valid ArraySegment to the same buffer returned by GetBuffer. + /// #if NET40 || NET45 public bool TryGetBuffer(out ArraySegment buffer) #else - public override bool TryGetBuffer(out ArraySegment buffer) + public override bool TryGetBuffer(out ArraySegment buffer) #endif - { - CheckDisposed(); - buffer = new ArraySegment(GetBuffer(), 0, (int)Length); - // GetBuffer has no failure modes, so this should always succeed - return true; - } + { + CheckDisposed(); + buffer = new ArraySegment(GetBuffer(), 0, (int)Length); + // GetBuffer has no failure modes, so this should always succeed + return true; + } - /// - /// Returns a new array with a copy of the buffer's contents. You should almost certainly be using GetBuffer combined with - /// the Length to - /// access the bytes in this stream. Calling ToArray will destroy the benefits of pooled buffers, but it is included - /// for the sake of completeness. - /// - /// Object has been disposed + /// + /// Returns a new array with a copy of the buffer's contents. You should almost certainly be using GetBuffer combined with + /// the Length to + /// access the bytes in this stream. Calling ToArray will destroy the benefits of pooled buffers, but it is included + /// for the sake of completeness. + /// + /// Object has been disposed #pragma warning disable CS0809 - [Obsolete("This method has degraded performance vs. GetBuffer and should be avoided.")] - public override byte[] ToArray() - { - CheckDisposed(); - var newBuffer = new byte[Length]; + [Obsolete("This method has degraded performance vs. GetBuffer and should be avoided.")] + public override byte[] ToArray() + { + CheckDisposed(); + var newBuffer = new byte[Length]; - InternalRead(newBuffer, 0, _length, 0); - var stack = _memoryManager.GenerateCallStacks ? Environment.StackTrace : null; - RecyclableMemoryStreamManager.EventsWriter.MemoryStreamToArray(_id, _tag, stack, 0); - _memoryManager.ReportStreamToArray(); + InternalRead(newBuffer, 0, _length, 0); + var stack = _memoryManager.GenerateCallStacks ? Environment.StackTrace : null; + RecyclableMemoryStreamManager.EventsWriter.MemoryStreamToArray(_id, _tag, stack, 0); + _memoryManager.ReportStreamToArray(); - return newBuffer; - } + return newBuffer; + } #pragma warning restore CS0809 - /// - /// Reads from the current position into the provided buffer - /// - /// Destination buffer - /// Offset into buffer at which to start placing the read bytes. - /// Number of bytes to read. - /// The number of bytes read - /// buffer is null - /// offset or count is less than 0 - /// offset subtracted from the buffer length is less than count - /// Object has been disposed - public override int Read(byte[] buffer, int offset, int count) => SafeRead(buffer, offset, count, ref _position); - - /// - /// Reads from the specified position into the provided buffer - /// - /// Destination buffer - /// Offset into buffer at which to start placing the read bytes. - /// Number of bytes to read. - /// Position in the stream to start reading from - /// The number of bytes read - /// buffer is null - /// offset or count is less than 0 - /// offset subtracted from the buffer length is less than count - /// Object has been disposed - public int SafeRead(byte[] buffer, int offset, int count, ref int streamPosition) - { - CheckDisposed(); - if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + /// + /// Reads from the current position into the provided buffer + /// + /// Destination buffer + /// Offset into buffer at which to start placing the read bytes. + /// Number of bytes to read. + /// The number of bytes read + /// buffer is null + /// offset or count is less than 0 + /// offset subtracted from the buffer length is less than count + /// Object has been disposed + public override int Read(byte[] buffer, int offset, int count) => SafeRead(buffer, offset, count, ref _position); - if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), "offset cannot be negative"); + /// + /// Reads from the specified position into the provided buffer + /// + /// Destination buffer + /// Offset into buffer at which to start placing the read bytes. + /// Number of bytes to read. + /// Position in the stream to start reading from + /// The number of bytes read + /// buffer is null + /// offset or count is less than 0 + /// offset subtracted from the buffer length is less than count + /// Object has been disposed + public int SafeRead(byte[] buffer, int offset, int count, ref int streamPosition) + { + CheckDisposed(); + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); - if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative"); + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), "offset cannot be negative"); - if (offset + count > buffer.Length) throw new ArgumentException("buffer length must be at least offset + count"); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative"); - var amountRead = InternalRead(buffer, offset, count, streamPosition); - streamPosition += amountRead; - return amountRead; - } + if (offset + count > buffer.Length) throw new ArgumentException("buffer length must be at least offset + count"); + + var amountRead = InternalRead(buffer, offset, count, streamPosition); + streamPosition += amountRead; + return amountRead; + } #if NETCOREAPP2_1 || NETSTANDARD2_1 /// @@ -547,7 +547,7 @@ public int SafeRead(byte[] buffer, int offset, int count, ref int streamPosition /// Object has been disposed public override int Read(Span buffer) => SafeRead(buffer, ref _position); - /// + /// /// Reads from the specified position into the provided buffer /// /// Destination buffer @@ -564,63 +564,63 @@ public int SafeRead(Span buffer, ref int streamPosition) } #endif - /// - /// Writes the buffer to the stream - /// - /// Source buffer - /// Start position - /// Number of bytes to write - /// buffer is null - /// offset or count is negative - /// buffer.Length - offset is not less than count - /// Object has been disposed - public override void Write(byte[] buffer, int offset, int count) - { - CheckDisposed(); - if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + /// + /// Writes the buffer to the stream + /// + /// Source buffer + /// Start position + /// Number of bytes to write + /// buffer is null + /// offset or count is negative + /// buffer.Length - offset is not less than count + /// Object has been disposed + public override void Write(byte[] buffer, int offset, int count) + { + CheckDisposed(); + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset), offset, - "Offset must be in the range of 0 - buffer.Length-1"); + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), offset, + "Offset must be in the range of 0 - buffer.Length-1"); - if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), count, "count must be non-negative"); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), count, "count must be non-negative"); - if (count + offset > buffer.Length) throw new ArgumentException("count must be greater than buffer.Length - offset"); + if (count + offset > buffer.Length) throw new ArgumentException("count must be greater than buffer.Length - offset"); - var blockSize = _memoryManager.BlockSize; - var end = (long)_position + count; - // Check for overflow - if (end > MaxStreamLength) throw new IOException("Maximum capacity exceeded"); + var blockSize = _memoryManager.BlockSize; + var end = (long)_position + count; + // Check for overflow + if (end > MaxStreamLength) throw new IOException("Maximum capacity exceeded"); - EnsureCapacity((int)end); + EnsureCapacity((int)end); - if (_largeBuffer == null) - { - var bytesRemaining = count; - var bytesWritten = 0; - var blockAndOffset = GetBlockAndRelativeOffset(_position); + if (_largeBuffer == null) + { + var bytesRemaining = count; + var bytesWritten = 0; + var blockAndOffset = GetBlockAndRelativeOffset(_position); - while (bytesRemaining > 0) - { - var currentBlock = _blocks[blockAndOffset.Block]; - var remainingInBlock = blockSize - blockAndOffset.Offset; - var amountToWriteInBlock = Math.Min(remainingInBlock, bytesRemaining); + while (bytesRemaining > 0) + { + var currentBlock = _blocks[blockAndOffset.Block]; + var remainingInBlock = blockSize - blockAndOffset.Offset; + var amountToWriteInBlock = Math.Min(remainingInBlock, bytesRemaining); - Buffer.BlockCopy(buffer, offset + bytesWritten, currentBlock, blockAndOffset.Offset, - amountToWriteInBlock); + Buffer.BlockCopy(buffer, offset + bytesWritten, currentBlock, blockAndOffset.Offset, + amountToWriteInBlock); - bytesRemaining -= amountToWriteInBlock; - bytesWritten += amountToWriteInBlock; + bytesRemaining -= amountToWriteInBlock; + bytesWritten += amountToWriteInBlock; - ++blockAndOffset.Block; - blockAndOffset.Offset = 0; - } + ++blockAndOffset.Block; + blockAndOffset.Offset = 0; } - else - Buffer.BlockCopy(buffer, offset, _largeBuffer, _position, count); - _position = (int)end; - _length = Math.Max(_position, _length); } + else + Buffer.BlockCopy(buffer, offset, _largeBuffer, _position, count); + _position = (int)end; + _length = Math.Max(_position, _length); + } #if NETCOREAPP2_1 || NETSTANDARD2_1 /// @@ -638,7 +638,7 @@ public override void Write(ReadOnlySpan source) // Check for overflow if (end > MaxStreamLength) throw new IOException("Maximum capacity exceeded"); - EnsureCapacity((int)end); + EnsureCapacity((int)end); if (_largeBuffer == null) { @@ -660,191 +660,191 @@ public override void Write(ReadOnlySpan source) } } else - source.CopyTo(_largeBuffer.AsSpan(_position)); - _position = (int)end; + source.CopyTo(_largeBuffer.AsSpan(_position)); + _position = (int)end; _length = Math.Max(_position, _length); } #endif - /// - /// Returns a useful string for debugging. This should not normally be called in actual production code. - /// - public override string ToString() => $"Id = {Id}, Tag = {Tag}, Length = {Length:N0} bytes"; - - /// - /// Writes a single byte to the current position in the stream. - /// - /// byte value to write - /// Object has been disposed - public override void WriteByte(byte value) - { - CheckDisposed(); - _byteBuffer[0] = value; - Write(_byteBuffer, 0, 1); - } + /// + /// Returns a useful string for debugging. This should not normally be called in actual production code. + /// + public override string ToString() => $"Id = {Id}, Tag = {Tag}, Length = {Length:N0} bytes"; - /// - /// Reads a single byte from the current position in the stream. - /// - /// The byte at the current position, or -1 if the position is at the end of the stream. - /// Object has been disposed - public override int ReadByte() => SafeReadByte(ref _position); - - /// - /// Reads a single byte from the specified position in the stream. - /// - /// The position in the stream to read from - /// The byte at the current position, or -1 if the position is at the end of the stream. - /// Object has been disposed - public int SafeReadByte(ref int streamPosition) - { - CheckDisposed(); - if (streamPosition == _length) return -1; + /// + /// Writes a single byte to the current position in the stream. + /// + /// byte value to write + /// Object has been disposed + public override void WriteByte(byte value) + { + CheckDisposed(); + _byteBuffer[0] = value; + Write(_byteBuffer, 0, 1); + } - byte value; - if (_largeBuffer == null) - { - var blockAndOffset = GetBlockAndRelativeOffset(streamPosition); - value = _blocks[blockAndOffset.Block][blockAndOffset.Offset]; - } - else - value = _largeBuffer[streamPosition]; - streamPosition++; - return value; - } + /// + /// Reads a single byte from the current position in the stream. + /// + /// The byte at the current position, or -1 if the position is at the end of the stream. + /// Object has been disposed + public override int ReadByte() => SafeReadByte(ref _position); + + /// + /// Reads a single byte from the specified position in the stream. + /// + /// The position in the stream to read from + /// The byte at the current position, or -1 if the position is at the end of the stream. + /// Object has been disposed + public int SafeReadByte(ref int streamPosition) + { + CheckDisposed(); + if (streamPosition == _length) return -1; - /// - /// Sets the length of the stream - /// - /// value is negative or larger than MaxStreamLength - /// Object has been disposed - public override void SetLength(long value) + byte value; + if (_largeBuffer == null) { - CheckDisposed(); - if (value < 0 || value > MaxStreamLength) - throw new ArgumentOutOfRangeException(nameof(value), - "value must be non-negative and at most " + MaxStreamLength); + var blockAndOffset = GetBlockAndRelativeOffset(streamPosition); + value = _blocks[blockAndOffset.Block][blockAndOffset.Offset]; + } + else + value = _largeBuffer[streamPosition]; + streamPosition++; + return value; + } - EnsureCapacity((int)value); + /// + /// Sets the length of the stream + /// + /// value is negative or larger than MaxStreamLength + /// Object has been disposed + public override void SetLength(long value) + { + CheckDisposed(); + if (value < 0 || value > MaxStreamLength) + throw new ArgumentOutOfRangeException(nameof(value), + "value must be non-negative and at most " + MaxStreamLength); - _length = (int)value; - if (_position > value) _position = (int)value; - } + EnsureCapacity((int)value); - /// - /// Sets the position to the offset from the seek location - /// - /// How many bytes to move - /// From where - /// The new position - /// Object has been disposed - /// offset is larger than MaxStreamLength - /// Invalid seek origin - /// Attempt to set negative position - public override long Seek(long offset, SeekOrigin loc) - { - CheckDisposed(); - if (offset > MaxStreamLength) throw new ArgumentOutOfRangeException(nameof(offset), "offset cannot be larger than " + MaxStreamLength); + _length = (int)value; + if (_position > value) _position = (int)value; + } - int newPosition; - switch (loc) - { - case SeekOrigin.Begin: - newPosition = (int)offset; - break; - case SeekOrigin.Current: - newPosition = (int)offset + _position; - break; - case SeekOrigin.End: - newPosition = (int)offset + _length; - break; - default: - throw new ArgumentException("Invalid seek origin", nameof(loc)); - } - if (newPosition < 0) throw new IOException("Seek before beginning"); + /// + /// Sets the position to the offset from the seek location + /// + /// How many bytes to move + /// From where + /// The new position + /// Object has been disposed + /// offset is larger than MaxStreamLength + /// Invalid seek origin + /// Attempt to set negative position + public override long Seek(long offset, SeekOrigin loc) + { + CheckDisposed(); + if (offset > MaxStreamLength) throw new ArgumentOutOfRangeException(nameof(offset), "offset cannot be larger than " + MaxStreamLength); - _position = newPosition; - return _position; + int newPosition; + switch (loc) + { + case SeekOrigin.Begin: + newPosition = (int)offset; + break; + case SeekOrigin.Current: + newPosition = (int)offset + _position; + break; + case SeekOrigin.End: + newPosition = (int)offset + _length; + break; + default: + throw new ArgumentException("Invalid seek origin", nameof(loc)); } + if (newPosition < 0) throw new IOException("Seek before beginning"); - /// - /// Synchronously writes this stream's bytes to the parameter stream. - /// - /// Destination stream - /// Important: This does a synchronous write, which may not be desired in some situations - public override void WriteTo(Stream stream) + _position = newPosition; + return _position; + } + + /// + /// Synchronously writes this stream's bytes to the parameter stream. + /// + /// Destination stream + /// Important: This does a synchronous write, which may not be desired in some situations + public override void WriteTo(Stream stream) + { + CheckDisposed(); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + if (_largeBuffer == null) { - CheckDisposed(); - if (stream == null) throw new ArgumentNullException(nameof(stream)); + var currentBlock = 0; + var bytesRemaining = _length; - if (_largeBuffer == null) + while (bytesRemaining > 0) { - var currentBlock = 0; - var bytesRemaining = _length; - - while (bytesRemaining > 0) - { - var amountToCopy = Math.Min(_blocks[currentBlock].Length, bytesRemaining); - stream.Write(_blocks[currentBlock], 0, amountToCopy); + var amountToCopy = Math.Min(_blocks[currentBlock].Length, bytesRemaining); + stream.Write(_blocks[currentBlock], 0, amountToCopy); - bytesRemaining -= amountToCopy; + bytesRemaining -= amountToCopy; - ++currentBlock; - } + ++currentBlock; } - else - stream.Write(_largeBuffer, 0, _length); } + else + stream.Write(_largeBuffer, 0, _length); + } - #endregion + #endregion - #region Helper Methods + #region Helper Methods - private bool Disposed => Interlocked.Read(ref _disposedState) != 0; + private bool Disposed => Interlocked.Read(ref _disposedState) != 0; - private void CheckDisposed() - { - if (Disposed) throw new ObjectDisposedException($"The stream with Id {_id} and Tag {_tag} is disposed."); - } + private void CheckDisposed() + { + if (Disposed) throw new ObjectDisposedException($"The stream with Id {_id} and Tag {_tag} is disposed."); + } - private int InternalRead(byte[] buffer, int offset, int count, int fromPosition) - { - if (_length - fromPosition <= 0) return 0; + private int InternalRead(byte[] buffer, int offset, int count, int fromPosition) + { + if (_length - fromPosition <= 0) return 0; - int amountToCopy; + int amountToCopy; + + if (_largeBuffer == null) + { + var blockAndOffset = GetBlockAndRelativeOffset(fromPosition); + var bytesWritten = 0; + var bytesRemaining = Math.Min(count, _length - fromPosition); - if (_largeBuffer == null) + while (bytesRemaining > 0) { - var blockAndOffset = GetBlockAndRelativeOffset(fromPosition); - var bytesWritten = 0; - var bytesRemaining = Math.Min(count, _length - fromPosition); - - while (bytesRemaining > 0) - { - amountToCopy = Math.Min(_blocks[blockAndOffset.Block].Length - blockAndOffset.Offset, - bytesRemaining); - Buffer.BlockCopy(_blocks[blockAndOffset.Block], blockAndOffset.Offset, buffer, - bytesWritten + offset, amountToCopy); - - bytesWritten += amountToCopy; - bytesRemaining -= amountToCopy; - - ++blockAndOffset.Block; - blockAndOffset.Offset = 0; - } - return bytesWritten; + amountToCopy = Math.Min(_blocks[blockAndOffset.Block].Length - blockAndOffset.Offset, + bytesRemaining); + Buffer.BlockCopy(_blocks[blockAndOffset.Block], blockAndOffset.Offset, buffer, + bytesWritten + offset, amountToCopy); + + bytesWritten += amountToCopy; + bytesRemaining -= amountToCopy; + + ++blockAndOffset.Block; + blockAndOffset.Offset = 0; } - amountToCopy = Math.Min(count, _length - fromPosition); - Buffer.BlockCopy(_largeBuffer, fromPosition, buffer, offset, amountToCopy); - return amountToCopy; + return bytesWritten; } + amountToCopy = Math.Min(count, _length - fromPosition); + Buffer.BlockCopy(_largeBuffer, fromPosition, buffer, offset, amountToCopy); + return amountToCopy; + } #if NETCOREAPP2_1 || NETSTANDARD2_1 private int InternalRead(Span buffer, int fromPosition) { if (_length - fromPosition <= 0) return 0; - int amountToCopy; + int amountToCopy; if (_largeBuffer == null) { @@ -873,69 +873,68 @@ private int InternalRead(Span buffer, int fromPosition) } #endif - private struct BlockAndOffset - { - public int Block; - public int Offset; + private struct BlockAndOffset + { + public int Block; + public int Offset; - public BlockAndOffset(int block, int offset) - { - Block = block; - Offset = offset; - } + public BlockAndOffset(int block, int offset) + { + Block = block; + Offset = offset; } + } + + private BlockAndOffset GetBlockAndRelativeOffset(int offset) + { + var blockSize = _memoryManager.BlockSize; + return new BlockAndOffset(offset / blockSize, offset % blockSize); + } - private BlockAndOffset GetBlockAndRelativeOffset(int offset) + private void EnsureCapacity(int newCapacity) + { + if (newCapacity > _memoryManager.MaximumStreamCapacity && _memoryManager.MaximumStreamCapacity > 0) { - var blockSize = _memoryManager.BlockSize; - return new BlockAndOffset(offset / blockSize, offset % blockSize); + RecyclableMemoryStreamManager.EventsWriter.MemoryStreamOverCapacity(newCapacity, + _memoryManager + .MaximumStreamCapacity, _tag, + AllocationStack); + throw new InvalidOperationException("Requested capacity is too large: " + newCapacity + ". Limit is " + + _memoryManager.MaximumStreamCapacity); } - private void EnsureCapacity(int newCapacity) + if (_largeBuffer != null) { - if (newCapacity > _memoryManager.MaximumStreamCapacity && _memoryManager.MaximumStreamCapacity > 0) - { - RecyclableMemoryStreamManager.EventsWriter.MemoryStreamOverCapacity(newCapacity, - _memoryManager - .MaximumStreamCapacity, _tag, - AllocationStack); - throw new InvalidOperationException("Requested capacity is too large: " + newCapacity + ". Limit is " + - _memoryManager.MaximumStreamCapacity); - } - - if (_largeBuffer != null) + if (newCapacity > _largeBuffer.Length) { - if (newCapacity > _largeBuffer.Length) - { - var newBuffer = _memoryManager.GetLargeBuffer(newCapacity, _tag); - InternalRead(newBuffer, 0, _length, 0); - ReleaseLargeBuffer(); - _largeBuffer = newBuffer; - } + var newBuffer = _memoryManager.GetLargeBuffer(newCapacity, _tag); + InternalRead(newBuffer, 0, _length, 0); + ReleaseLargeBuffer(); + _largeBuffer = newBuffer; } - else - while (Capacity < newCapacity) - _blocks.Add(_memoryManager.GetBlock()); } + else + while (Capacity < newCapacity) + _blocks.Add(_memoryManager.GetBlock()); + } - /// - /// Release the large buffer (either stores it for eventual release or returns it immediately). - /// - private void ReleaseLargeBuffer() + /// + /// Release the large buffer (either stores it for eventual release or returns it immediately). + /// + private void ReleaseLargeBuffer() + { + if (_memoryManager.AggressiveBufferReturn) + _memoryManager.ReturnLargeBuffer(_largeBuffer, _tag); + else { - if (_memoryManager.AggressiveBufferReturn) - _memoryManager.ReturnLargeBuffer(_largeBuffer, _tag); - else - { - if (_dirtyBuffers == null) - // We most likely will only ever need space for one - _dirtyBuffers = new List(1); - _dirtyBuffers.Add(_largeBuffer); - } - - _largeBuffer = null; + if (_dirtyBuffers == null) + // We most likely will only ever need space for one + _dirtyBuffers = new List(1); + _dirtyBuffers.Add(_largeBuffer); } - #endregion + _largeBuffer = null; } + + #endregion } diff --git a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamFactory.cs b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamFactory.cs index 240f182..0983f7b 100644 --- a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamFactory.cs +++ b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamFactory.cs @@ -5,43 +5,41 @@ using System; using System.IO; -namespace Elastic.Transport -{ - /// - /// A factory for creating memory streams using a recyclable pool of instances - /// - public sealed class RecyclableMemoryStreamFactory : IMemoryStreamFactory - { - private const string TagSource = "Elastic.Transport"; - private readonly RecyclableMemoryStreamManager _manager; +namespace Elastic.Transport; - /// Provide a static instance of this stateless class, so it can be reused - public static RecyclableMemoryStreamFactory Default { get; } = new RecyclableMemoryStreamFactory(); +/// +/// A factory for creating memory streams using a recyclable pool of instances +/// +public sealed class RecyclableMemoryStreamFactory : MemoryStreamFactory +{ + private const string TagSource = "Elastic.Transport"; + private readonly RecyclableMemoryStreamManager _manager; - /// - public RecyclableMemoryStreamFactory() => _manager = CreateManager(experimental: false); + /// Provide a static instance of this stateless class, so it can be reused + public static RecyclableMemoryStreamFactory Default { get; } = new RecyclableMemoryStreamFactory(); - private static RecyclableMemoryStreamManager CreateManager(bool experimental) - { - if (!experimental) return new RecyclableMemoryStreamManager() { AggressiveBufferReturn = true }; + /// + public RecyclableMemoryStreamFactory() => _manager = CreateManager(experimental: false); - const int blockSize = 1024; - const int largeBufferMultiple = 1024 * 1024; - const int maxBufferSize = 16 * largeBufferMultiple; - return new RecyclableMemoryStreamManager(blockSize, largeBufferMultiple, maxBufferSize) - { - AggressiveBufferReturn = true, MaximumFreeLargePoolBytes = maxBufferSize * 4, MaximumFreeSmallPoolBytes = 100 * blockSize - }; + private static RecyclableMemoryStreamManager CreateManager(bool experimental) + { + if (!experimental) return new RecyclableMemoryStreamManager() { AggressiveBufferReturn = true }; - } + const int blockSize = 1024; + const int largeBufferMultiple = 1024 * 1024; + const int maxBufferSize = 16 * largeBufferMultiple; + return new RecyclableMemoryStreamManager(blockSize, largeBufferMultiple, maxBufferSize) + { + AggressiveBufferReturn = true, MaximumFreeLargePoolBytes = maxBufferSize * 4, MaximumFreeSmallPoolBytes = 100 * blockSize + }; + } - /// - public MemoryStream Create() => _manager.GetStream(Guid.Empty, TagSource); + /// + public override MemoryStream Create() => _manager.GetStream(Guid.Empty, TagSource); - /// - public MemoryStream Create(byte[] bytes) => _manager.GetStream(bytes); + /// + public override MemoryStream Create(byte[] bytes) => _manager.GetStream(bytes); - /// - public MemoryStream Create(byte[] bytes, int index, int count) => _manager.GetStream(Guid.Empty, TagSource, bytes, index, count); - } + /// + public override MemoryStream Create(byte[] bytes, int index, int count) => _manager.GetStream(Guid.Empty, TagSource, bytes, index, count); } diff --git a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager-Events.cs b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager-Events.cs index 50352ef..00ff313 100644 --- a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager-Events.cs +++ b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager-Events.cs @@ -30,130 +30,129 @@ using System.Diagnostics.Tracing; using System.Threading; -namespace Elastic.Transport -{ +namespace Elastic.Transport; + #if !DOTNETCORE - /// - /// Stub for System.Diagnostics.Tracing.EventCounter which is not available on .NET 4.6.1 - /// - // ReSharper disable once UnusedType.Global - internal class EventCounter - { - // ReSharper disable UnusedParameter.Local - public EventCounter(string blocks, RecyclableMemoryStreamManager.Events eventsWriter) { } - // ReSharper restore UnusedParameter.Local +/// +/// Stub for System.Diagnostics.Tracing.EventCounter which is not available on .NET 4.6.1 +/// +// ReSharper disable once UnusedType.Global +internal sealed class EventCounter +{ + // ReSharper disable UnusedParameter.Local + public EventCounter(string blocks, RecyclableMemoryStreamManager.Events eventsWriter) { } + // ReSharper restore UnusedParameter.Local - public void WriteMetric(long v) { } - } + public void WriteMetric(long v) { } +} #endif #if NETSTANDARD2_0 || NETFRAMEWORK - internal class PollingCounter : IDisposable - { - // ReSharper disable UnusedParameter.Local - public PollingCounter(string largeBuffers, RecyclableMemoryStreamManager.Events eventsWriter, Func func) { } - // ReSharper restore UnusedParameter.Local +internal sealed class PollingCounter : IDisposable +{ + // ReSharper disable UnusedParameter.Local + public PollingCounter(string largeBuffers, RecyclableMemoryStreamManager.Events eventsWriter, Func func) { } + // ReSharper restore UnusedParameter.Local - public void Dispose() {} - } + public void Dispose() {} +} #endif - internal sealed partial class RecyclableMemoryStreamManager +internal sealed partial class RecyclableMemoryStreamManager +{ + public static readonly Events EventsWriter = new Events(); + + [EventSource(Name = "Elastic-Transport-RecyclableMemoryStream", Guid = "{AD44FDAC-D3FC-460A-9EBE-E55A3569A8F6}")] + public sealed class Events : EventSource { - public static readonly Events EventsWriter = new Events(); - - [EventSource(Name = "Elastic-Transport-RecyclableMemoryStream", Guid = "{AD44FDAC-D3FC-460A-9EBE-E55A3569A8F6}")] - public sealed class Events : EventSource + + public enum MemoryStreamBufferType { + Small, + Large + } + + public enum MemoryStreamDiscardReason + { + TooLarge, + EnoughFree + } + + [Event(1, Level = EventLevel.Verbose)] + public void MemoryStreamCreated(Guid guid, string tag, int requestedSize) + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) + WriteEvent(1, guid, tag ?? string.Empty, requestedSize); + } + + [Event(2, Level = EventLevel.Verbose)] + public void MemoryStreamDisposed(Guid guid, string tag) + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) + WriteEvent(2, guid, tag ?? string.Empty); + } - public enum MemoryStreamBufferType - { - Small, - Large - } - - public enum MemoryStreamDiscardReason - { - TooLarge, - EnoughFree - } - - [Event(1, Level = EventLevel.Verbose)] - public void MemoryStreamCreated(Guid guid, string tag, int requestedSize) - { - if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) - WriteEvent(1, guid, tag ?? string.Empty, requestedSize); - } - - [Event(2, Level = EventLevel.Verbose)] - public void MemoryStreamDisposed(Guid guid, string tag) - { - if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) - WriteEvent(2, guid, tag ?? string.Empty); - } - - [Event(3, Level = EventLevel.Critical)] - public void MemoryStreamDoubleDispose(Guid guid, string tag, string allocationStack, string disposeStack1, string disposeStack2) - { - if (IsEnabled()) - WriteEvent(3, guid, tag ?? string.Empty, allocationStack ?? string.Empty, - disposeStack1 ?? string.Empty, disposeStack2 ?? string.Empty); - } - - [Event(4, Level = EventLevel.Error)] - public void MemoryStreamFinalized(Guid guid, string tag, string allocationStack) - { - if (IsEnabled()) - WriteEvent(4, guid, tag ?? string.Empty, allocationStack ?? string.Empty); - } - - [Event(5, Level = EventLevel.Verbose)] - public void MemoryStreamToArray(Guid guid, string tag, string stack, int size) - { - if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) - WriteEvent(5, guid, tag ?? string.Empty, stack ?? string.Empty, size); - } - - [Event(6, Level = EventLevel.Informational)] - public void MemoryStreamManagerInitialized(int blockSize, int largeBufferMultiple, int maximumBufferSize) - { - if (IsEnabled()) - WriteEvent(6, blockSize, largeBufferMultiple, maximumBufferSize); - } - - [Event(7, Level = EventLevel.Verbose)] - public void MemoryStreamNewBlockCreated(long smallPoolInUseBytes) - { - if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) - WriteEvent(7, smallPoolInUseBytes); - } - - [Event(8, Level = EventLevel.Verbose)] - public void MemoryStreamNewLargeBufferCreated(int requiredSize, long largePoolInUseBytes) - { - if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) - WriteEvent(8, requiredSize, largePoolInUseBytes); - } - - [Event(9, Level = EventLevel.Verbose)] - public void MemoryStreamNonPooledLargeBufferCreated(int requiredSize, string tag, string allocationStack) - { - if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) - WriteEvent(9, requiredSize, tag ?? string.Empty, allocationStack ?? string.Empty); - } - - [Event(10, Level = EventLevel.Warning)] - public void MemoryStreamDiscardBuffer(MemoryStreamBufferType bufferType, string tag, MemoryStreamDiscardReason reason) - { - if (IsEnabled()) - WriteEvent(10, bufferType, tag ?? string.Empty, reason); - } - - [Event(11, Level = EventLevel.Error)] - public void MemoryStreamOverCapacity(int requestedCapacity, long maxCapacity, string tag, string allocationStack) - { - if (IsEnabled()) - WriteEvent(11, requestedCapacity, maxCapacity, tag ?? string.Empty, allocationStack ?? string.Empty); - } + [Event(3, Level = EventLevel.Critical)] + public void MemoryStreamDoubleDispose(Guid guid, string tag, string allocationStack, string disposeStack1, string disposeStack2) + { + if (IsEnabled()) + WriteEvent(3, guid, tag ?? string.Empty, allocationStack ?? string.Empty, + disposeStack1 ?? string.Empty, disposeStack2 ?? string.Empty); + } + + [Event(4, Level = EventLevel.Error)] + public void MemoryStreamFinalized(Guid guid, string tag, string allocationStack) + { + if (IsEnabled()) + WriteEvent(4, guid, tag ?? string.Empty, allocationStack ?? string.Empty); + } + + [Event(5, Level = EventLevel.Verbose)] + public void MemoryStreamToArray(Guid guid, string tag, string stack, int size) + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) + WriteEvent(5, guid, tag ?? string.Empty, stack ?? string.Empty, size); + } + + [Event(6, Level = EventLevel.Informational)] + public void MemoryStreamManagerInitialized(int blockSize, int largeBufferMultiple, int maximumBufferSize) + { + if (IsEnabled()) + WriteEvent(6, blockSize, largeBufferMultiple, maximumBufferSize); + } + + [Event(7, Level = EventLevel.Verbose)] + public void MemoryStreamNewBlockCreated(long smallPoolInUseBytes) + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) + WriteEvent(7, smallPoolInUseBytes); + } + + [Event(8, Level = EventLevel.Verbose)] + public void MemoryStreamNewLargeBufferCreated(int requiredSize, long largePoolInUseBytes) + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) + WriteEvent(8, requiredSize, largePoolInUseBytes); + } + + [Event(9, Level = EventLevel.Verbose)] + public void MemoryStreamNonPooledLargeBufferCreated(int requiredSize, string tag, string allocationStack) + { + if (IsEnabled(EventLevel.Verbose, EventKeywords.None)) + WriteEvent(9, requiredSize, tag ?? string.Empty, allocationStack ?? string.Empty); + } + + [Event(10, Level = EventLevel.Warning)] + public void MemoryStreamDiscardBuffer(MemoryStreamBufferType bufferType, string tag, MemoryStreamDiscardReason reason) + { + if (IsEnabled()) + WriteEvent(10, bufferType, tag ?? string.Empty, reason); + } + + [Event(11, Level = EventLevel.Error)] + public void MemoryStreamOverCapacity(int requestedCapacity, long maxCapacity, string tag, string allocationStack) + { + if (IsEnabled()) + WriteEvent(11, requestedCapacity, maxCapacity, tag ?? string.Empty, allocationStack ?? string.Empty); } } } diff --git a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager.cs b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager.cs index 32fe11b..51eb515 100644 --- a/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager.cs +++ b/src/Elastic.Transport/Components/Providers/RecyclableMemoryStreamManager.cs @@ -30,663 +30,662 @@ using System.IO; using System.Threading; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Manages pools of RecyclableMemoryStream objects. +/// +/// +/// There are two pools managed in here. The small pool contains same-sized buffers that are handed to streams +/// as they write more data. +/// For scenarios that need to call GetBuffer(), the large pool contains buffers of various sizes, all +/// multiples/exponentials of LargeBufferMultiple (1 MB by default). They are split by size to avoid overly-wasteful buffer +/// usage. There should be far fewer 8 MB buffers than 1 MB buffers, for example. +/// +internal sealed partial class RecyclableMemoryStreamManager { /// - /// Manages pools of RecyclableMemoryStream objects. + /// Generic delegate for handling events without any arguments. /// - /// - /// There are two pools managed in here. The small pool contains same-sized buffers that are handed to streams - /// as they write more data. - /// For scenarios that need to call GetBuffer(), the large pool contains buffers of various sizes, all - /// multiples/exponentials of LargeBufferMultiple (1 MB by default). They are split by size to avoid overly-wasteful buffer - /// usage. There should be far fewer 8 MB buffers than 1 MB buffers, for example. - /// - internal partial class RecyclableMemoryStreamManager + public delegate void EventHandler(); + + /// + /// Delegate for handling large buffer discard reports. + /// + /// Reason the buffer was discarded. + public delegate void LargeBufferDiscardedEventHandler(Events.MemoryStreamDiscardReason reason); + + /// + /// Delegate for handling reports of stream size when streams are allocated + /// + /// Bytes allocated. + public delegate void StreamLengthReportHandler(long bytes); + + /// + /// Delegate for handling periodic reporting of memory use statistics. + /// + /// Bytes currently in use in the small pool. + /// Bytes currently free in the small pool. + /// Bytes currently in use in the large pool. + /// Bytes currently free in the large pool. + public delegate void UsageReportEventHandler( + long smallPoolInUseBytes, long smallPoolFreeBytes, long largePoolInUseBytes, long largePoolFreeBytes + ); + + public const int DefaultBlockSize = 128 * 1024; + public const int DefaultLargeBufferMultiple = 1024 * 1024; + public const int DefaultMaximumBufferSize = 128 * 1024 * 1024; + + private readonly long[] _largeBufferFreeSize; + private readonly long[] _largeBufferInUseSize; + + + /// + /// pools[0] = 1x largeBufferMultiple buffers + /// pools[1] = 2x largeBufferMultiple buffers + /// pools[2] = 3x(multiple)/4x(exponential) largeBufferMultiple buffers + /// etc., up to maximumBufferSize + /// + private readonly ConcurrentStack[] _largePools; + + private readonly ConcurrentStack _smallPool; + + private long _smallPoolFreeSize; + private long _smallPoolInUseSize; + + /// + /// Initializes the memory manager with the default block/buffer specifications. + /// + public RecyclableMemoryStreamManager() + : this(DefaultBlockSize, DefaultLargeBufferMultiple, DefaultMaximumBufferSize, false) { } + + /// + /// Initializes the memory manager with the given block requiredSize. + /// + /// Size of each block that is pooled. Must be > 0. + /// Each large buffer will be a multiple of this value. + /// Buffers larger than this are not pooled + /// + /// blockSize is not a positive number, or largeBufferMultiple is not a + /// positive number, or maximumBufferSize is less than blockSize. + /// + /// maximumBufferSize is not a multiple of largeBufferMultiple + public RecyclableMemoryStreamManager(int blockSize, int largeBufferMultiple, int maximumBufferSize) + : this(blockSize, largeBufferMultiple, maximumBufferSize, false) { } + + /// + /// Initializes the memory manager with the given block requiredSize. + /// + /// Size of each block that is pooled. Must be > 0. + /// Each large buffer will be a multiple/exponential of this value. + /// Buffers larger than this are not pooled + /// Switch to exponential large buffer allocation strategy + /// + /// blockSize is not a positive number, or largeBufferMultiple is not a + /// positive number, or maximumBufferSize is less than blockSize. + /// + /// maximumBufferSize is not a multiple/exponential of largeBufferMultiple + public RecyclableMemoryStreamManager(int blockSize, int largeBufferMultiple, int maximumBufferSize, bool useExponentialLargeBuffer) { - /// - /// Generic delegate for handling events without any arguments. - /// - public delegate void EventHandler(); - - /// - /// Delegate for handling large buffer discard reports. - /// - /// Reason the buffer was discarded. - public delegate void LargeBufferDiscardedEventHandler(Events.MemoryStreamDiscardReason reason); - - /// - /// Delegate for handling reports of stream size when streams are allocated - /// - /// Bytes allocated. - public delegate void StreamLengthReportHandler(long bytes); - - /// - /// Delegate for handling periodic reporting of memory use statistics. - /// - /// Bytes currently in use in the small pool. - /// Bytes currently free in the small pool. - /// Bytes currently in use in the large pool. - /// Bytes currently free in the large pool. - public delegate void UsageReportEventHandler( - long smallPoolInUseBytes, long smallPoolFreeBytes, long largePoolInUseBytes, long largePoolFreeBytes - ); - - public const int DefaultBlockSize = 128 * 1024; - public const int DefaultLargeBufferMultiple = 1024 * 1024; - public const int DefaultMaximumBufferSize = 128 * 1024 * 1024; - - private readonly long[] _largeBufferFreeSize; - private readonly long[] _largeBufferInUseSize; - - - /// - /// pools[0] = 1x largeBufferMultiple buffers - /// pools[1] = 2x largeBufferMultiple buffers - /// pools[2] = 3x(multiple)/4x(exponential) largeBufferMultiple buffers - /// etc., up to maximumBufferSize - /// - private readonly ConcurrentStack[] _largePools; - - private readonly ConcurrentStack _smallPool; - - private long _smallPoolFreeSize; - private long _smallPoolInUseSize; - - /// - /// Initializes the memory manager with the default block/buffer specifications. - /// - public RecyclableMemoryStreamManager() - : this(DefaultBlockSize, DefaultLargeBufferMultiple, DefaultMaximumBufferSize, false) { } - - /// - /// Initializes the memory manager with the given block requiredSize. - /// - /// Size of each block that is pooled. Must be > 0. - /// Each large buffer will be a multiple of this value. - /// Buffers larger than this are not pooled - /// - /// blockSize is not a positive number, or largeBufferMultiple is not a - /// positive number, or maximumBufferSize is less than blockSize. - /// - /// maximumBufferSize is not a multiple of largeBufferMultiple - public RecyclableMemoryStreamManager(int blockSize, int largeBufferMultiple, int maximumBufferSize) - : this(blockSize, largeBufferMultiple, maximumBufferSize, false) { } - - /// - /// Initializes the memory manager with the given block requiredSize. - /// - /// Size of each block that is pooled. Must be > 0. - /// Each large buffer will be a multiple/exponential of this value. - /// Buffers larger than this are not pooled - /// Switch to exponential large buffer allocation strategy - /// - /// blockSize is not a positive number, or largeBufferMultiple is not a - /// positive number, or maximumBufferSize is less than blockSize. - /// - /// maximumBufferSize is not a multiple/exponential of largeBufferMultiple - public RecyclableMemoryStreamManager(int blockSize, int largeBufferMultiple, int maximumBufferSize, bool useExponentialLargeBuffer) - { - if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize), blockSize, "blockSize must be a positive number"); + if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize), blockSize, "blockSize must be a positive number"); - if (largeBufferMultiple <= 0) - throw new ArgumentOutOfRangeException(nameof(largeBufferMultiple), - "largeBufferMultiple must be a positive number"); + if (largeBufferMultiple <= 0) + throw new ArgumentOutOfRangeException(nameof(largeBufferMultiple), + "largeBufferMultiple must be a positive number"); - if (maximumBufferSize < blockSize) - throw new ArgumentOutOfRangeException(nameof(maximumBufferSize), - "maximumBufferSize must be at least blockSize"); + if (maximumBufferSize < blockSize) + throw new ArgumentOutOfRangeException(nameof(maximumBufferSize), + "maximumBufferSize must be at least blockSize"); - BlockSize = blockSize; - LargeBufferMultiple = largeBufferMultiple; - MaximumBufferSize = maximumBufferSize; - UseExponentialLargeBuffer = useExponentialLargeBuffer; + BlockSize = blockSize; + LargeBufferMultiple = largeBufferMultiple; + MaximumBufferSize = maximumBufferSize; + UseExponentialLargeBuffer = useExponentialLargeBuffer; - if (!IsLargeBufferSize(maximumBufferSize)) - throw new ArgumentException(string.Format("maximumBufferSize is not {0} of largeBufferMultiple", - UseExponentialLargeBuffer ? "an exponential" : "a multiple"), - nameof(maximumBufferSize)); + if (!IsLargeBufferSize(maximumBufferSize)) + throw new ArgumentException(string.Format("maximumBufferSize is not {0} of largeBufferMultiple", + UseExponentialLargeBuffer ? "an exponential" : "a multiple"), + nameof(maximumBufferSize)); - _smallPool = new ConcurrentStack(); - var numLargePools = useExponentialLargeBuffer - // ReSharper disable once PossibleLossOfFraction - // Not our code assume loss is intentional - ? (int)Math.Log(maximumBufferSize / largeBufferMultiple, 2) + 1 - : maximumBufferSize / largeBufferMultiple; + _smallPool = new ConcurrentStack(); + var numLargePools = useExponentialLargeBuffer + // ReSharper disable once PossibleLossOfFraction + // Not our code assume loss is intentional + ? (int)Math.Log(maximumBufferSize / largeBufferMultiple, 2) + 1 + : maximumBufferSize / largeBufferMultiple; - // +1 to store size of bytes in use that are too large to be pooled - _largeBufferInUseSize = new long[numLargePools + 1]; - _largeBufferFreeSize = new long[numLargePools]; + // +1 to store size of bytes in use that are too large to be pooled + _largeBufferInUseSize = new long[numLargePools + 1]; + _largeBufferFreeSize = new long[numLargePools]; - _largePools = new ConcurrentStack[numLargePools]; + _largePools = new ConcurrentStack[numLargePools]; - for (var i = 0; i < _largePools.Length; ++i) _largePools[i] = new ConcurrentStack(); + for (var i = 0; i < _largePools.Length; ++i) _largePools[i] = new ConcurrentStack(); - EventsWriter.MemoryStreamManagerInitialized(blockSize, largeBufferMultiple, maximumBufferSize); - } + EventsWriter.MemoryStreamManagerInitialized(blockSize, largeBufferMultiple, maximumBufferSize); + } + + /// + /// Whether dirty buffers can be immediately returned to the buffer pool. E.g. when GetBuffer() is called on + /// a stream and creates a single large buffer, if this setting is enabled, the other blocks will be returned + /// to the buffer pool immediately. + /// Note when enabling this setting that the user is responsible for ensuring that any buffer previously + /// retrieved from a stream which is subsequently modified is not used after modification (as it may no longer + /// be valid). + /// + public bool AggressiveBufferReturn { get; set; } + + /// + /// The size of each block. It must be set at creation and cannot be changed. + /// + public int BlockSize { get; } + + /// + /// Whether to save callstacks for stream allocations. This can help in debugging. + /// It should NEVER be turned on generally in production. + /// + public bool GenerateCallStacks { get; set; } + + /// + /// All buffers are multiples/exponentials of this number. It must be set at creation and cannot be changed. + /// + public int LargeBufferMultiple { get; } - /// - /// Whether dirty buffers can be immediately returned to the buffer pool. E.g. when GetBuffer() is called on - /// a stream and creates a single large buffer, if this setting is enabled, the other blocks will be returned - /// to the buffer pool immediately. - /// Note when enabling this setting that the user is responsible for ensuring that any buffer previously - /// retrieved from a stream which is subsequently modified is not used after modification (as it may no longer - /// be valid). - /// - public bool AggressiveBufferReturn { get; set; } - - /// - /// The size of each block. It must be set at creation and cannot be changed. - /// - public int BlockSize { get; } - - /// - /// Whether to save callstacks for stream allocations. This can help in debugging. - /// It should NEVER be turned on generally in production. - /// - public bool GenerateCallStacks { get; set; } - - /// - /// All buffers are multiples/exponentials of this number. It must be set at creation and cannot be changed. - /// - public int LargeBufferMultiple { get; } - - /// - /// How many buffers are in the large pool - /// - public long LargeBuffersFree + /// + /// How many buffers are in the large pool + /// + public long LargeBuffersFree + { + get { - get - { - long free = 0; - foreach (var pool in _largePools) free += pool.Count; - return free; - } + long free = 0; + foreach (var pool in _largePools) free += pool.Count; + return free; } + } - /// - /// Number of bytes in large pool not currently in use - /// - public long LargePoolFreeSize + /// + /// Number of bytes in large pool not currently in use + /// + public long LargePoolFreeSize + { + get { - get - { - long sum = 0; - foreach (var freeSize in _largeBufferFreeSize) sum += freeSize; + long sum = 0; + foreach (var freeSize in _largeBufferFreeSize) sum += freeSize; - return sum; - } + return sum; } + } - /// - /// Number of bytes currently in use by streams from the large pool - /// - public long LargePoolInUseSize + /// + /// Number of bytes currently in use by streams from the large pool + /// + public long LargePoolInUseSize + { + get { - get - { - long sum = 0; - foreach (var inUseSize in _largeBufferInUseSize) sum += inUseSize; + long sum = 0; + foreach (var inUseSize in _largeBufferInUseSize) sum += inUseSize; - return sum; - } + return sum; } + } - /// - /// Gets the maximum buffer size. - /// - /// - /// Any buffer that is returned to the pool that is larger than this will be - /// discarded and garbage collected. - /// - public int MaximumBufferSize { get; } - - /// - /// How many bytes of large free buffers to allow before we start dropping - /// those returned to us. - /// - public long MaximumFreeLargePoolBytes { get; set; } - - /// - /// How many bytes of small free blocks to allow before we start dropping - /// those returned to us. - /// - public long MaximumFreeSmallPoolBytes { get; set; } - - /// - /// Maximum stream capacity in bytes. Attempts to set a larger capacity will - /// result in an exception. - /// - /// A value of 0 indicates no limit. - public long MaximumStreamCapacity { get; set; } - - /// - /// How many blocks are in the small pool - /// - public long SmallBlocksFree => _smallPool.Count; - - /// - /// Number of bytes in small pool not currently in use - /// - public long SmallPoolFreeSize => _smallPoolFreeSize; - - /// - /// Number of bytes currently in use by stream from the small pool - /// - public long SmallPoolInUseSize => _smallPoolInUseSize; - - /// - /// Use exponential large buffer allocation strategy. It must be set at creation and cannot be changed. - /// - public bool UseExponentialLargeBuffer { get; } - - /// - /// Use multiple large buffer allocation strategy. It must be set at creation and cannot be changed. - /// - public bool UseMultipleLargeBuffer => !UseExponentialLargeBuffer; - - /// - /// Removes and returns a single block from the pool. - /// - /// A byte[] array - internal byte[] GetBlock() - { - if (!_smallPool.TryPop(out var block)) - { - // We'll add this back to the pool when the stream is disposed - // (unless our free pool is too large) - block = new byte[BlockSize]; - EventsWriter.MemoryStreamNewBlockCreated(_smallPoolInUseSize); - ReportBlockCreated(); - } - else - Interlocked.Add(ref _smallPoolFreeSize, -BlockSize); + /// + /// Gets the maximum buffer size. + /// + /// + /// Any buffer that is returned to the pool that is larger than this will be + /// discarded and garbage collected. + /// + public int MaximumBufferSize { get; } - Interlocked.Add(ref _smallPoolInUseSize, BlockSize); - return block; - } + /// + /// How many bytes of large free buffers to allow before we start dropping + /// those returned to us. + /// + public long MaximumFreeLargePoolBytes { get; set; } - /// - /// Returns a buffer of arbitrary size from the large buffer pool. This buffer - /// will be at least the requiredSize and always be a multiple/exponential of largeBufferMultiple. - /// - /// The minimum length of the buffer - /// The tag of the stream returning this buffer, for logging if necessary. - /// A buffer of at least the required size. - internal byte[] GetLargeBuffer(int requiredSize, string tag) - { - requiredSize = RoundToLargeBufferSize(requiredSize); + /// + /// How many bytes of small free blocks to allow before we start dropping + /// those returned to us. + /// + public long MaximumFreeSmallPoolBytes { get; set; } - var poolIndex = GetPoolIndex(requiredSize); + /// + /// Maximum stream capacity in bytes. Attempts to set a larger capacity will + /// result in an exception. + /// + /// A value of 0 indicates no limit. + public long MaximumStreamCapacity { get; set; } - byte[] buffer; - if (poolIndex < _largePools.Length) - { - if (!_largePools[poolIndex].TryPop(out buffer)) - { - buffer = new byte[requiredSize]; - - EventsWriter.MemoryStreamNewLargeBufferCreated(requiredSize, LargePoolInUseSize); - ReportLargeBufferCreated(); - } - else - Interlocked.Add(ref _largeBufferFreeSize[poolIndex], -buffer.Length); - } - else - { - // Buffer is too large to pool. They get a new buffer. + /// + /// How many blocks are in the small pool + /// + public long SmallBlocksFree => _smallPool.Count; - // We still want to track the size, though, and we've reserved a slot - // in the end of the inuse array for nonpooled bytes in use. - poolIndex = _largeBufferInUseSize.Length - 1; + /// + /// Number of bytes in small pool not currently in use + /// + public long SmallPoolFreeSize => _smallPoolFreeSize; - // We still want to round up to reduce heap fragmentation. - buffer = new byte[requiredSize]; - string callStack = null; - if (GenerateCallStacks) - // Grab the stack -- we want to know who requires such large buffers - callStack = Environment.StackTrace; - EventsWriter.MemoryStreamNonPooledLargeBufferCreated(requiredSize, tag, callStack); - ReportLargeBufferCreated(); - } + /// + /// Number of bytes currently in use by stream from the small pool + /// + public long SmallPoolInUseSize => _smallPoolInUseSize; + + /// + /// Use exponential large buffer allocation strategy. It must be set at creation and cannot be changed. + /// + public bool UseExponentialLargeBuffer { get; } - Interlocked.Add(ref _largeBufferInUseSize[poolIndex], buffer.Length); + /// + /// Use multiple large buffer allocation strategy. It must be set at creation and cannot be changed. + /// + public bool UseMultipleLargeBuffer => !UseExponentialLargeBuffer; - return buffer; + /// + /// Removes and returns a single block from the pool. + /// + /// A byte[] array + internal byte[] GetBlock() + { + if (!_smallPool.TryPop(out var block)) + { + // We'll add this back to the pool when the stream is disposed + // (unless our free pool is too large) + block = new byte[BlockSize]; + EventsWriter.MemoryStreamNewBlockCreated(_smallPoolInUseSize); + ReportBlockCreated(); } + else + Interlocked.Add(ref _smallPoolFreeSize, -BlockSize); + + Interlocked.Add(ref _smallPoolInUseSize, BlockSize); + return block; + } - private int RoundToLargeBufferSize(int requiredSize) + /// + /// Returns a buffer of arbitrary size from the large buffer pool. This buffer + /// will be at least the requiredSize and always be a multiple/exponential of largeBufferMultiple. + /// + /// The minimum length of the buffer + /// The tag of the stream returning this buffer, for logging if necessary. + /// A buffer of at least the required size. + internal byte[] GetLargeBuffer(int requiredSize, string tag) + { + requiredSize = RoundToLargeBufferSize(requiredSize); + + var poolIndex = GetPoolIndex(requiredSize); + + byte[] buffer; + if (poolIndex < _largePools.Length) { - if (UseExponentialLargeBuffer) + if (!_largePools[poolIndex].TryPop(out buffer)) { - var pow = 1; - while (LargeBufferMultiple * pow < requiredSize) pow <<= 1; - return LargeBufferMultiple * pow; + buffer = new byte[requiredSize]; + + EventsWriter.MemoryStreamNewLargeBufferCreated(requiredSize, LargePoolInUseSize); + ReportLargeBufferCreated(); } else - return (requiredSize + LargeBufferMultiple - 1) / LargeBufferMultiple * LargeBufferMultiple; + Interlocked.Add(ref _largeBufferFreeSize[poolIndex], -buffer.Length); + } + else + { + // Buffer is too large to pool. They get a new buffer. + + // We still want to track the size, though, and we've reserved a slot + // in the end of the inuse array for nonpooled bytes in use. + poolIndex = _largeBufferInUseSize.Length - 1; + + // We still want to round up to reduce heap fragmentation. + buffer = new byte[requiredSize]; + string callStack = null; + if (GenerateCallStacks) + // Grab the stack -- we want to know who requires such large buffers + callStack = Environment.StackTrace; + EventsWriter.MemoryStreamNonPooledLargeBufferCreated(requiredSize, tag, callStack); + ReportLargeBufferCreated(); } - private bool IsLargeBufferSize(int value) => - value != 0 && (UseExponentialLargeBuffer - ? value == RoundToLargeBufferSize(value) - : value % LargeBufferMultiple == 0); + Interlocked.Add(ref _largeBufferInUseSize[poolIndex], buffer.Length); - private int GetPoolIndex(int length) + return buffer; + } + + private int RoundToLargeBufferSize(int requiredSize) + { + if (UseExponentialLargeBuffer) { - if (UseExponentialLargeBuffer) - { - var index = 0; - while (LargeBufferMultiple << index < length) ++index; - return index; - } - else - return length / LargeBufferMultiple - 1; + var pow = 1; + while (LargeBufferMultiple * pow < requiredSize) pow <<= 1; + return LargeBufferMultiple * pow; } + else + return (requiredSize + LargeBufferMultiple - 1) / LargeBufferMultiple * LargeBufferMultiple; + } - /// - /// Returns the buffer to the large pool - /// - /// The buffer to return. - /// The tag of the stream returning this buffer, for logging if necessary. - /// buffer is null - /// - /// buffer.Length is not a multiple/exponential of LargeBufferMultiple (it did not - /// originate from this pool) - /// - internal void ReturnLargeBuffer(byte[] buffer, string tag) + private bool IsLargeBufferSize(int value) => + value != 0 && (UseExponentialLargeBuffer + ? value == RoundToLargeBufferSize(value) + : value % LargeBufferMultiple == 0); + + private int GetPoolIndex(int length) + { + if (UseExponentialLargeBuffer) { - if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + var index = 0; + while (LargeBufferMultiple << index < length) ++index; + return index; + } + else + return length / LargeBufferMultiple - 1; + } - if (!IsLargeBufferSize(buffer.Length)) - throw new ArgumentException( - string.Format("buffer did not originate from this memory manager. The size is not {0} of ", - UseExponentialLargeBuffer ? "an exponential" : "a multiple") + - LargeBufferMultiple); + /// + /// Returns the buffer to the large pool + /// + /// The buffer to return. + /// The tag of the stream returning this buffer, for logging if necessary. + /// buffer is null + /// + /// buffer.Length is not a multiple/exponential of LargeBufferMultiple (it did not + /// originate from this pool) + /// + internal void ReturnLargeBuffer(byte[] buffer, string tag) + { + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + + if (!IsLargeBufferSize(buffer.Length)) + throw new ArgumentException( + string.Format("buffer did not originate from this memory manager. The size is not {0} of ", + UseExponentialLargeBuffer ? "an exponential" : "a multiple") + + LargeBufferMultiple); - var poolIndex = GetPoolIndex(buffer.Length); + var poolIndex = GetPoolIndex(buffer.Length); - if (poolIndex < _largePools.Length) + if (poolIndex < _largePools.Length) + { + if ((_largePools[poolIndex].Count + 1) * buffer.Length <= MaximumFreeLargePoolBytes || + MaximumFreeLargePoolBytes == 0) { - if ((_largePools[poolIndex].Count + 1) * buffer.Length <= MaximumFreeLargePoolBytes || - MaximumFreeLargePoolBytes == 0) - { - _largePools[poolIndex].Push(buffer); - Interlocked.Add(ref _largeBufferFreeSize[poolIndex], buffer.Length); - } - else - { - EventsWriter.MemoryStreamDiscardBuffer(Events.MemoryStreamBufferType.Large, tag, - Events.MemoryStreamDiscardReason.EnoughFree); - ReportLargeBufferDiscarded(Events.MemoryStreamDiscardReason.EnoughFree); - } + _largePools[poolIndex].Push(buffer); + Interlocked.Add(ref _largeBufferFreeSize[poolIndex], buffer.Length); } else { - // This is a non-poolable buffer, but we still want to track its size for inuse - // analysis. We have space in the inuse array for this. - poolIndex = _largeBufferInUseSize.Length - 1; - EventsWriter.MemoryStreamDiscardBuffer(Events.MemoryStreamBufferType.Large, tag, - Events.MemoryStreamDiscardReason.TooLarge); - ReportLargeBufferDiscarded(Events.MemoryStreamDiscardReason.TooLarge); + Events.MemoryStreamDiscardReason.EnoughFree); + ReportLargeBufferDiscarded(Events.MemoryStreamDiscardReason.EnoughFree); } + } + else + { + // This is a non-poolable buffer, but we still want to track its size for inuse + // analysis. We have space in the inuse array for this. + poolIndex = _largeBufferInUseSize.Length - 1; - Interlocked.Add(ref _largeBufferInUseSize[poolIndex], -buffer.Length); - - ReportUsageReport(_smallPoolInUseSize, _smallPoolFreeSize, LargePoolInUseSize, - LargePoolFreeSize); + EventsWriter.MemoryStreamDiscardBuffer(Events.MemoryStreamBufferType.Large, tag, + Events.MemoryStreamDiscardReason.TooLarge); + ReportLargeBufferDiscarded(Events.MemoryStreamDiscardReason.TooLarge); } - /// - /// Returns the blocks to the pool - /// - /// Collection of blocks to return to the pool - /// The tag of the stream returning these blocks, for logging if necessary. - /// blocks is null - /// blocks contains buffers that are the wrong size (or null) for this memory manager - internal void ReturnBlocks(ICollection blocks, string tag) - { - if (blocks == null) throw new ArgumentNullException(nameof(blocks)); + Interlocked.Add(ref _largeBufferInUseSize[poolIndex], -buffer.Length); + + ReportUsageReport(_smallPoolInUseSize, _smallPoolFreeSize, LargePoolInUseSize, + LargePoolFreeSize); + } + + /// + /// Returns the blocks to the pool + /// + /// Collection of blocks to return to the pool + /// The tag of the stream returning these blocks, for logging if necessary. + /// blocks is null + /// blocks contains buffers that are the wrong size (or null) for this memory manager + internal void ReturnBlocks(ICollection blocks, string tag) + { + if (blocks == null) throw new ArgumentNullException(nameof(blocks)); - var bytesToReturn = blocks.Count * BlockSize; - Interlocked.Add(ref _smallPoolInUseSize, -bytesToReturn); + var bytesToReturn = blocks.Count * BlockSize; + Interlocked.Add(ref _smallPoolInUseSize, -bytesToReturn); - foreach (var block in blocks) + foreach (var block in blocks) + { + if (block == null || block.Length != BlockSize) + throw new ArgumentException("blocks contains buffers that are not BlockSize in length"); + } + + foreach (var block in blocks) + { + if (MaximumFreeSmallPoolBytes == 0 || SmallPoolFreeSize < MaximumFreeSmallPoolBytes) { - if (block == null || block.Length != BlockSize) - throw new ArgumentException("blocks contains buffers that are not BlockSize in length"); + Interlocked.Add(ref _smallPoolFreeSize, BlockSize); + _smallPool.Push(block); } - - foreach (var block in blocks) + else { - if (MaximumFreeSmallPoolBytes == 0 || SmallPoolFreeSize < MaximumFreeSmallPoolBytes) - { - Interlocked.Add(ref _smallPoolFreeSize, BlockSize); - _smallPool.Push(block); - } - else - { - EventsWriter.MemoryStreamDiscardBuffer(Events.MemoryStreamBufferType.Small, tag, - Events.MemoryStreamDiscardReason.EnoughFree); - ReportBlockDiscarded(); - break; - } + EventsWriter.MemoryStreamDiscardBuffer(Events.MemoryStreamBufferType.Small, tag, + Events.MemoryStreamDiscardReason.EnoughFree); + ReportBlockDiscarded(); + break; } - - ReportUsageReport(_smallPoolInUseSize, _smallPoolFreeSize, LargePoolInUseSize, - LargePoolFreeSize); } - internal void ReportBlockCreated() => BlockCreated?.Invoke(); + ReportUsageReport(_smallPoolInUseSize, _smallPoolFreeSize, LargePoolInUseSize, + LargePoolFreeSize); + } - internal void ReportBlockDiscarded() => BlockDiscarded?.Invoke(); + internal void ReportBlockCreated() => BlockCreated?.Invoke(); - internal void ReportLargeBufferCreated() => LargeBufferCreated?.Invoke(); + internal void ReportBlockDiscarded() => BlockDiscarded?.Invoke(); - internal void ReportLargeBufferDiscarded(Events.MemoryStreamDiscardReason reason) => LargeBufferDiscarded?.Invoke(reason); + internal void ReportLargeBufferCreated() => LargeBufferCreated?.Invoke(); - internal void ReportStreamCreated() => StreamCreated?.Invoke(); + internal void ReportLargeBufferDiscarded(Events.MemoryStreamDiscardReason reason) => LargeBufferDiscarded?.Invoke(reason); - internal void ReportStreamDisposed() => StreamDisposed?.Invoke(); + internal void ReportStreamCreated() => StreamCreated?.Invoke(); - internal void ReportStreamFinalized() => StreamFinalized?.Invoke(); + internal void ReportStreamDisposed() => StreamDisposed?.Invoke(); - internal void ReportStreamLength(long bytes) => StreamLength?.Invoke(bytes); + internal void ReportStreamFinalized() => StreamFinalized?.Invoke(); - internal void ReportStreamToArray() => StreamConvertedToArray?.Invoke(); + internal void ReportStreamLength(long bytes) => StreamLength?.Invoke(bytes); - internal void ReportUsageReport( - long smallPoolInUseBytes, long smallPoolFreeBytes, long largePoolInUseBytes, long largePoolFreeBytes - ) => - UsageReport?.Invoke(smallPoolInUseBytes, smallPoolFreeBytes, largePoolInUseBytes, largePoolFreeBytes); + internal void ReportStreamToArray() => StreamConvertedToArray?.Invoke(); - /// - /// Retrieve a new MemoryStream object with no tag and a default initial capacity. - /// - /// A MemoryStream. - public MemoryStream GetStream() => new RecyclableMemoryStream(this); + internal void ReportUsageReport( + long smallPoolInUseBytes, long smallPoolFreeBytes, long largePoolInUseBytes, long largePoolFreeBytes + ) => + UsageReport?.Invoke(smallPoolInUseBytes, smallPoolFreeBytes, largePoolInUseBytes, largePoolFreeBytes); - private class ReportingMemoryStream : MemoryStream - { - private readonly RecyclableMemoryStreamManager _instance; + /// + /// Retrieve a new MemoryStream object with no tag and a default initial capacity. + /// + /// A MemoryStream. + public MemoryStream GetStream() => new RecyclableMemoryStream(this); - public ReportingMemoryStream(byte[] bytes, RecyclableMemoryStreamManager instance) : base(bytes) => _instance = instance; - } + private class ReportingMemoryStream : MemoryStream + { + private readonly RecyclableMemoryStreamManager _instance; - /// - /// Shortcut to create a stream that directly wraps bytes but still uses reporting on the stream being created and disposes. - /// Note this does NOT use the pooled memory streams as the bytes have already been allocated - /// - public MemoryStream GetStream(byte[] bytes) => new ReportingMemoryStream(bytes, this); - - /// - /// Retrieve a new MemoryStream object with no tag and a default initial capacity. - /// - /// A unique identifier which can be used to trace usages of the stream. - /// A MemoryStream. - public MemoryStream GetStream(Guid id) => new RecyclableMemoryStream(this, id); - - /// - /// Retrieve a new MemoryStream object with the given tag and a default initial capacity. - /// - /// A tag which can be used to track the source of the stream. - /// A MemoryStream. - public MemoryStream GetStream(string tag) => new RecyclableMemoryStream(this, tag); - - /// - /// Retrieve a new MemoryStream object with the given tag and a default initial capacity. - /// - /// A unique identifier which can be used to trace usages of the stream. - /// A tag which can be used to track the source of the stream. - /// A MemoryStream. - public MemoryStream GetStream(Guid id, string tag) => new RecyclableMemoryStream(this, id, tag); - - /// - /// Retrieve a new MemoryStream object with the given tag and at least the given capacity. - /// - /// A tag which can be used to track the source of the stream. - /// The minimum desired capacity for the stream. - /// A MemoryStream. - public MemoryStream GetStream(string tag, int requiredSize) => new RecyclableMemoryStream(this, tag, requiredSize); - - /// - /// Retrieve a new MemoryStream object with the given tag and at least the given capacity. - /// - /// A unique identifier which can be used to trace usages of the stream. - /// A tag which can be used to track the source of the stream. - /// The minimum desired capacity for the stream. - /// A MemoryStream. - public MemoryStream GetStream(Guid id, string tag, int requiredSize) => new RecyclableMemoryStream(this, id, tag, requiredSize); - - /// - /// Retrieve a new MemoryStream object with the given tag and at least the given capacity, possibly using - /// a single contiguous underlying buffer. - /// - /// - /// Retrieving a MemoryStream which provides a single contiguous buffer can be useful in situations - /// where the initial size is known and it is desirable to avoid copying data between the smaller underlying - /// buffers to a single large one. This is most helpful when you know that you will always call GetBuffer - /// on the underlying stream. - /// - /// A unique identifier which can be used to trace usages of the stream. - /// A tag which can be used to track the source of the stream. - /// The minimum desired capacity for the stream. - /// Whether to attempt to use a single contiguous buffer. - /// A MemoryStream. - public MemoryStream GetStream(Guid id, string tag, int requiredSize, bool asContiguousBuffer) - { - if (!asContiguousBuffer || requiredSize <= BlockSize) return GetStream(id, tag, requiredSize); + public ReportingMemoryStream(byte[] bytes, RecyclableMemoryStreamManager instance) : base(bytes) => _instance = instance; + } - return new RecyclableMemoryStream(this, id, tag, requiredSize, GetLargeBuffer(requiredSize, tag)); - } + /// + /// Shortcut to create a stream that directly wraps bytes but still uses reporting on the stream being created and disposes. + /// Note this does NOT use the pooled memory streams as the bytes have already been allocated + /// + public MemoryStream GetStream(byte[] bytes) => new ReportingMemoryStream(bytes, this); + + /// + /// Retrieve a new MemoryStream object with no tag and a default initial capacity. + /// + /// A unique identifier which can be used to trace usages of the stream. + /// A MemoryStream. + public MemoryStream GetStream(Guid id) => new RecyclableMemoryStream(this, id); + + /// + /// Retrieve a new MemoryStream object with the given tag and a default initial capacity. + /// + /// A tag which can be used to track the source of the stream. + /// A MemoryStream. + public MemoryStream GetStream(string tag) => new RecyclableMemoryStream(this, tag); + + /// + /// Retrieve a new MemoryStream object with the given tag and a default initial capacity. + /// + /// A unique identifier which can be used to trace usages of the stream. + /// A tag which can be used to track the source of the stream. + /// A MemoryStream. + public MemoryStream GetStream(Guid id, string tag) => new RecyclableMemoryStream(this, id, tag); + + /// + /// Retrieve a new MemoryStream object with the given tag and at least the given capacity. + /// + /// A tag which can be used to track the source of the stream. + /// The minimum desired capacity for the stream. + /// A MemoryStream. + public MemoryStream GetStream(string tag, int requiredSize) => new RecyclableMemoryStream(this, tag, requiredSize); + + /// + /// Retrieve a new MemoryStream object with the given tag and at least the given capacity. + /// + /// A unique identifier which can be used to trace usages of the stream. + /// A tag which can be used to track the source of the stream. + /// The minimum desired capacity for the stream. + /// A MemoryStream. + public MemoryStream GetStream(Guid id, string tag, int requiredSize) => new RecyclableMemoryStream(this, id, tag, requiredSize); - /// - /// Retrieve a new MemoryStream object with the given tag and at least the given capacity, possibly using - /// a single contiguous underlying buffer. - /// - /// - /// Retrieving a MemoryStream which provides a single contiguous buffer can be useful in situations - /// where the initial size is known and it is desirable to avoid copying data between the smaller underlying - /// buffers to a single large one. This is most helpful when you know that you will always call GetBuffer - /// on the underlying stream. - /// - /// A tag which can be used to track the source of the stream. - /// The minimum desired capacity for the stream. - /// Whether to attempt to use a single contiguous buffer. - /// A MemoryStream. - public MemoryStream GetStream(string tag, int requiredSize, bool asContiguousBuffer) => - GetStream(Guid.NewGuid(), tag, requiredSize, asContiguousBuffer); - - /// - /// Retrieve a new MemoryStream object with the given tag and with contents copied from the provided - /// buffer. The provided buffer is not wrapped or used after construction. - /// - /// The new stream's position is set to the beginning of the stream when returned. - /// A unique identifier which can be used to trace usages of the stream. - /// A tag which can be used to track the source of the stream. - /// The byte buffer to copy data from. - /// The offset from the start of the buffer to copy from. - /// The number of bytes to copy from the buffer. - /// A MemoryStream. - public MemoryStream GetStream(Guid id, string tag, byte[] buffer, int offset, int count) + /// + /// Retrieve a new MemoryStream object with the given tag and at least the given capacity, possibly using + /// a single contiguous underlying buffer. + /// + /// + /// Retrieving a MemoryStream which provides a single contiguous buffer can be useful in situations + /// where the initial size is known and it is desirable to avoid copying data between the smaller underlying + /// buffers to a single large one. This is most helpful when you know that you will always call GetBuffer + /// on the underlying stream. + /// + /// A unique identifier which can be used to trace usages of the stream. + /// A tag which can be used to track the source of the stream. + /// The minimum desired capacity for the stream. + /// Whether to attempt to use a single contiguous buffer. + /// A MemoryStream. + public MemoryStream GetStream(Guid id, string tag, int requiredSize, bool asContiguousBuffer) + { + if (!asContiguousBuffer || requiredSize <= BlockSize) return GetStream(id, tag, requiredSize); + + return new RecyclableMemoryStream(this, id, tag, requiredSize, GetLargeBuffer(requiredSize, tag)); + } + + /// + /// Retrieve a new MemoryStream object with the given tag and at least the given capacity, possibly using + /// a single contiguous underlying buffer. + /// + /// + /// Retrieving a MemoryStream which provides a single contiguous buffer can be useful in situations + /// where the initial size is known and it is desirable to avoid copying data between the smaller underlying + /// buffers to a single large one. This is most helpful when you know that you will always call GetBuffer + /// on the underlying stream. + /// + /// A tag which can be used to track the source of the stream. + /// The minimum desired capacity for the stream. + /// Whether to attempt to use a single contiguous buffer. + /// A MemoryStream. + public MemoryStream GetStream(string tag, int requiredSize, bool asContiguousBuffer) => + GetStream(Guid.NewGuid(), tag, requiredSize, asContiguousBuffer); + + /// + /// Retrieve a new MemoryStream object with the given tag and with contents copied from the provided + /// buffer. The provided buffer is not wrapped or used after construction. + /// + /// The new stream's position is set to the beginning of the stream when returned. + /// A unique identifier which can be used to trace usages of the stream. + /// A tag which can be used to track the source of the stream. + /// The byte buffer to copy data from. + /// The offset from the start of the buffer to copy from. + /// The number of bytes to copy from the buffer. + /// A MemoryStream. + public MemoryStream GetStream(Guid id, string tag, byte[] buffer, int offset, int count) + { + RecyclableMemoryStream stream = null; + try { - RecyclableMemoryStream stream = null; - try - { - stream = new RecyclableMemoryStream(this, id, tag, count); - stream.Write(buffer, offset, count); - stream.Position = 0; - return stream; - } - catch - { - stream?.Dispose(); - throw; - } + stream = new RecyclableMemoryStream(this, id, tag, count); + stream.Write(buffer, offset, count); + stream.Position = 0; + return stream; + } + catch + { + stream?.Dispose(); + throw; } + } - /// - /// Retrieve a new MemoryStream object with the given tag and with contents copied from the provided - /// buffer. The provided buffer is not wrapped or used after construction. - /// - /// The new stream's position is set to the beginning of the stream when returned. - /// A tag which can be used to track the source of the stream. - /// The byte buffer to copy data from. - /// The offset from the start of the buffer to copy from. - /// The number of bytes to copy from the buffer. - /// A MemoryStream. - public MemoryStream GetStream(string tag, byte[] buffer, int offset, int count) => GetStream(Guid.NewGuid(), tag, buffer, offset, count); - - /// - /// Triggered when a new block is created. - /// - public event EventHandler BlockCreated; - - /// - /// Triggered when a new block is created. - /// - public event EventHandler BlockDiscarded; - - /// - /// Triggered when a new large buffer is created. - /// - public event EventHandler LargeBufferCreated; - - /// - /// Triggered when a new stream is created. - /// - public event EventHandler StreamCreated; - - /// - /// Triggered when a stream is disposed. - /// - public event EventHandler StreamDisposed; - - /// - /// Triggered when a stream is finalized. - /// - public event EventHandler StreamFinalized; - - /// - /// Triggered when a stream is finalized. - /// - public event StreamLengthReportHandler StreamLength; - - /// - /// Triggered when a user converts a stream to array. - /// - public event EventHandler StreamConvertedToArray; - - /// - /// Triggered when a large buffer is discarded, along with the reason for the discard. - /// - public event LargeBufferDiscardedEventHandler LargeBufferDiscarded; - - /// - /// Periodically triggered to report usage statistics. - /// - public event UsageReportEventHandler UsageReport; + /// + /// Retrieve a new MemoryStream object with the given tag and with contents copied from the provided + /// buffer. The provided buffer is not wrapped or used after construction. + /// + /// The new stream's position is set to the beginning of the stream when returned. + /// A tag which can be used to track the source of the stream. + /// The byte buffer to copy data from. + /// The offset from the start of the buffer to copy from. + /// The number of bytes to copy from the buffer. + /// A MemoryStream. + public MemoryStream GetStream(string tag, byte[] buffer, int offset, int count) => GetStream(Guid.NewGuid(), tag, buffer, offset, count); + + /// + /// Triggered when a new block is created. + /// + public event EventHandler BlockCreated; + + /// + /// Triggered when a new block is created. + /// + public event EventHandler BlockDiscarded; + + /// + /// Triggered when a new large buffer is created. + /// + public event EventHandler LargeBufferCreated; + + /// + /// Triggered when a new stream is created. + /// + public event EventHandler StreamCreated; + + /// + /// Triggered when a stream is disposed. + /// + public event EventHandler StreamDisposed; + + /// + /// Triggered when a stream is finalized. + /// + public event EventHandler StreamFinalized; + + /// + /// Triggered when a stream is finalized. + /// + public event StreamLengthReportHandler StreamLength; + + /// + /// Triggered when a user converts a stream to array. + /// + public event EventHandler StreamConvertedToArray; + + /// + /// Triggered when a large buffer is discarded, along with the reason for the discard. + /// + public event LargeBufferDiscardedEventHandler LargeBufferDiscarded; + + /// + /// Periodically triggered to report usage statistics. + /// + public event UsageReportEventHandler UsageReport; - } } diff --git a/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs b/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs new file mode 100644 index 0000000..7c0ba62 --- /dev/null +++ b/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Transport; + +/// A factory that creates instances of , this factory exists so that transport can be tested. +public abstract class RequestPipelineFactory + where TConfiguration : class, ITransportConfiguration +{ + internal RequestPipelineFactory() { } + + /// Create an instance of + public abstract RequestPipeline Create(TConfiguration configuration, DateTimeProvider dateTimeProvider, + MemoryStreamFactory memoryStreamFactory, RequestParameters requestParameters); +} diff --git a/src/Elastic.Transport/Components/Serialization/Converters/DynamicDictionaryConverter.cs b/src/Elastic.Transport/Components/Serialization/Converters/DynamicDictionaryConverter.cs index 942bdc3..454cce4 100644 --- a/src/Elastic.Transport/Components/Serialization/Converters/DynamicDictionaryConverter.cs +++ b/src/Elastic.Transport/Components/Serialization/Converters/DynamicDictionaryConverter.cs @@ -9,40 +9,39 @@ using System.Text.Json.Serialization; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Elastic.Transport +namespace Elastic.Transport; + +internal class DynamicDictionaryConverter : JsonConverter { - internal class DynamicDictionaryConverter : JsonConverter + public override DynamicDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override DynamicDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.StartArray) { - if (reader.TokenType == JsonTokenType.StartArray) - { - var array = JsonSerializer.Deserialize(ref reader, options); - var arrayDict = new Dictionary(); - for (var i = 0; i < array.Length; i++) - arrayDict[i.ToString(CultureInfo.InvariantCulture)] = new DynamicValue(array[i]); - return DynamicDictionary.Create(arrayDict); - } - if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); - - var dict = JsonSerializer.Deserialize>(ref reader, options); - return DynamicDictionary.Create(dict); + var array = JsonSerializer.Deserialize(ref reader, options); + var arrayDict = new Dictionary(); + for (var i = 0; i < array.Length; i++) + arrayDict[i.ToString(CultureInfo.InvariantCulture)] = new DynamicValue(array[i]); + return DynamicDictionary.Create(arrayDict); } + if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); - public override void Write(Utf8JsonWriter writer, DynamicDictionary dictionary, JsonSerializerOptions options) - { - writer.WriteStartObject(); + var dict = JsonSerializer.Deserialize>(ref reader, options); + return DynamicDictionary.Create(dict); + } - foreach (var kvp in dictionary) - { - if (kvp.Value == null) continue; + public override void Write(Utf8JsonWriter writer, DynamicDictionary dictionary, JsonSerializerOptions options) + { + writer.WriteStartObject(); - writer.WritePropertyName(kvp.Key); + foreach (var kvp in dictionary) + { + if (kvp.Value == null) continue; - JsonSerializer.Serialize(writer, kvp.Value?.Value, options); - } + writer.WritePropertyName(kvp.Key); - writer.WriteEndObject(); + JsonSerializer.Serialize(writer, kvp.Value?.Value, options); } + + writer.WriteEndObject(); } } diff --git a/src/Elastic.Transport/Components/Serialization/Converters/ErrorCauseConverter.cs b/src/Elastic.Transport/Components/Serialization/Converters/ErrorCauseConverter.cs index ef09efe..5e704a5 100644 --- a/src/Elastic.Transport/Components/Serialization/Converters/ErrorCauseConverter.cs +++ b/src/Elastic.Transport/Components/Serialization/Converters/ErrorCauseConverter.cs @@ -10,211 +10,209 @@ using Elastic.Transport.Extensions; using Elastic.Transport.Products.Elasticsearch; -namespace Elastic.Transport -{ - internal class ErrorCauseConverter : ErrorCauseConverter { } +namespace Elastic.Transport; + +internal class ErrorCauseConverter : ErrorCauseConverter { } - internal class ErrorConverter : ErrorCauseConverter +internal class ErrorConverter : ErrorCauseConverter +{ + protected override bool ReadMore(ref Utf8JsonReader reader, JsonSerializerOptions options, string propertyName, Error errorCause) { - protected override bool ReadMore(ref Utf8JsonReader reader, JsonSerializerOptions options, string propertyName, Error errorCause) + void ReadAssign(ref Utf8JsonReader r, Action set) => + set(errorCause, JsonSerializer.Deserialize(ref r, options)); + switch (propertyName) { - void ReadAssign(ref Utf8JsonReader r, Action set) => - set(errorCause, JsonSerializer.Deserialize(ref r, options)); - switch (propertyName) - { - case "headers": - ReadAssign>(ref reader, (e, v) => e.Headers = v); - return true; + case "headers": + ReadAssign>(ref reader, (e, v) => e.Headers = v); + return true; - case "root_cause": - ReadAssign>(ref reader, (e, v) => e.RootCause = v); - return true; - default: - return false; + case "root_cause": + ReadAssign>(ref reader, (e, v) => e.RootCause = v); + return true; + default: + return false; - } } } +} - internal class ErrorCauseConverter : JsonConverter where TErrorCause : ErrorCause, new() +internal class ErrorCauseConverter : JsonConverter where TErrorCause : ErrorCause, new() +{ + public override TErrorCause Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override TErrorCause Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType != JsonTokenType.StartObject) { - if (reader.TokenType != JsonTokenType.StartObject) - { - return reader.TokenType == JsonTokenType.String - ? new TErrorCause { Reason = reader.GetString() } - : null; - } + return reader.TokenType == JsonTokenType.String + ? new TErrorCause { Reason = reader.GetString() } + : null; + } - var errorCause = new TErrorCause(); - var additionalProperties = new Dictionary(); - errorCause.AdditionalProperties = additionalProperties; + var errorCause = new TErrorCause(); + var additionalProperties = new Dictionary(); + errorCause.AdditionalProperties = additionalProperties; - void ReadAssign(ref Utf8JsonReader r, Action set) => - set(errorCause, JsonSerializer.Deserialize(ref r, options)); - void ReadAny(ref Utf8JsonReader r, string property, Action set) => - set(errorCause, property, JsonSerializer.Deserialize(ref r, options)); + void ReadAssign(ref Utf8JsonReader r, Action set) => + set(errorCause, JsonSerializer.Deserialize(ref r, options)); + void ReadAny(ref Utf8JsonReader r, string property, Action set) => + set(errorCause, property, JsonSerializer.Deserialize(ref r, options)); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) return errorCause; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) return errorCause; - if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); + if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); - var propertyName = reader.GetString(); - switch (propertyName) - { - //case "bytes_limit": - // ReadAssign(ref reader, (e, v) => e.BytesLimit = v); - // break; - //case "bytes_wanted": - // ReadAssign(ref reader, (e, v) => e.BytesWanted = v); - // break; - case "caused_by": - ReadAssign(ref reader, (e, v) => e.CausedBy = v); - break; - //case "col": - // ReadAssign(ref reader, (e, v) => e.Column = v); - // break; - //case "failed_shards": - // ReadAssign>(ref reader, (e, v) => e.FailedShards = v); - // break; - //case "grouped": - // ReadAssign(ref reader, (e, v) => e.Grouped = v); - // break; - case "index": - ReadAssign(ref reader, (e, v) => e.Index = v); - break; - case "index_uuid": - ReadAssign(ref reader, (e, v) => e.IndexUUID = v); - break; - //case "lang": - // ReadAssign(ref reader, (e, v) => e.Language = v); - // break; - //case "license.expired.feature": - // ReadAssign(ref reader, (e, v) => e.LicensedExpiredFeature = v); - // break; - //case "line": - // ReadAssign(ref reader, (e, v) => e.Line = v); - // break; - //case "phase": - // ReadAssign(ref reader, (e, v) => e.Phase = v); - // break; - case "reason": - ReadAssign(ref reader, (e, v) => e.Reason = v); - break; - //case "resource.id": - // errorCause.ResourceId = ReadSingleOrCollection(ref reader, options); - // break; - //case "resource.type": - // ReadAssign(ref reader, (e, v) => e.ResourceType = v); - // break; - //case "script": - // ReadAssign(ref reader, (e, v) => e.Script = v); - // break; - //case "script_stack": - // errorCause.ScriptStack = ReadSingleOrCollection(ref reader, options); - // break; - //case "shard": - // errorCause.Shard = ReadIntFromString(ref reader, options); - // break; - case "stack_trace": - ReadAssign(ref reader, (e, v) => e.StackTrace = v); - break; - case "type": - ReadAssign(ref reader, (e, v) => e.Type = v); + var propertyName = reader.GetString(); + switch (propertyName) + { + //case "bytes_limit": + // ReadAssign(ref reader, (e, v) => e.BytesLimit = v); + // break; + //case "bytes_wanted": + // ReadAssign(ref reader, (e, v) => e.BytesWanted = v); + // break; + case "caused_by": + ReadAssign(ref reader, (e, v) => e.CausedBy = v); + break; + //case "col": + // ReadAssign(ref reader, (e, v) => e.Column = v); + // break; + //case "failed_shards": + // ReadAssign>(ref reader, (e, v) => e.FailedShards = v); + // break; + //case "grouped": + // ReadAssign(ref reader, (e, v) => e.Grouped = v); + // break; + case "index": + ReadAssign(ref reader, (e, v) => e.Index = v); + break; + case "index_uuid": + ReadAssign(ref reader, (e, v) => e.IndexUUID = v); + break; + //case "lang": + // ReadAssign(ref reader, (e, v) => e.Language = v); + // break; + //case "license.expired.feature": + // ReadAssign(ref reader, (e, v) => e.LicensedExpiredFeature = v); + // break; + //case "line": + // ReadAssign(ref reader, (e, v) => e.Line = v); + // break; + //case "phase": + // ReadAssign(ref reader, (e, v) => e.Phase = v); + // break; + case "reason": + ReadAssign(ref reader, (e, v) => e.Reason = v); + break; + //case "resource.id": + // errorCause.ResourceId = ReadSingleOrCollection(ref reader, options); + // break; + //case "resource.type": + // ReadAssign(ref reader, (e, v) => e.ResourceType = v); + // break; + //case "script": + // ReadAssign(ref reader, (e, v) => e.Script = v); + // break; + //case "script_stack": + // errorCause.ScriptStack = ReadSingleOrCollection(ref reader, options); + // break; + //case "shard": + // errorCause.Shard = ReadIntFromString(ref reader, options); + // break; + case "stack_trace": + ReadAssign(ref reader, (e, v) => e.StackTrace = v); + break; + case "type": + ReadAssign(ref reader, (e, v) => e.Type = v); + break; + default: + if (ReadMore(ref reader, options, propertyName, errorCause)) break; + else + { + ReadAny(ref reader, propertyName, (e, p, v) => additionalProperties.Add(p, v)); break; - default: - if (ReadMore(ref reader, options, propertyName, errorCause)) break; - else - { - ReadAny(ref reader, propertyName, (e, p, v) => additionalProperties.Add(p, v)); - break; - } - } + } } - return errorCause; } + return errorCause; + } - - private static int? ReadIntFromString(ref Utf8JsonReader reader, JsonSerializerOptions options) + private static int? ReadIntFromString(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + reader.Read(); + switch (reader.TokenType) { - reader.Read(); - switch (reader.TokenType) - { - case JsonTokenType.Null: return null; - case JsonTokenType.Number: - return JsonSerializer.Deserialize(ref reader, options); - case JsonTokenType.String: - var s = JsonSerializer.Deserialize(ref reader, options); - if (int.TryParse(s, out var i)) return i; - return null; - default: - reader.TrySkip(); - return null; - } + case JsonTokenType.Null: return null; + case JsonTokenType.Number: + return JsonSerializer.Deserialize(ref reader, options); + case JsonTokenType.String: + var s = JsonSerializer.Deserialize(ref reader, options); + if (int.TryParse(s, out var i)) return i; + return null; + default: + reader.TrySkip(); + return null; } + } - private static IReadOnlyCollection ReadSingleOrCollection(ref Utf8JsonReader reader, JsonSerializerOptions options) + private static IReadOnlyCollection ReadSingleOrCollection(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + reader.Read(); + switch (reader.TokenType) { - reader.Read(); - switch (reader.TokenType) - { - case JsonTokenType.Null: return EmptyReadOnly.Collection; - case JsonTokenType.StartArray: - var list = new List(); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - break; - list.Add(JsonSerializer.Deserialize(ref reader, options)); - } - return new ReadOnlyCollection(list); - default: - var v = JsonSerializer.Deserialize(ref reader, options); - return new ReadOnlyCollection(new List(1) { v}); - } + case JsonTokenType.Null: return EmptyReadOnly.Collection; + case JsonTokenType.StartArray: + var list = new List(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + list.Add(JsonSerializer.Deserialize(ref reader, options)); + } + return new ReadOnlyCollection(list); + default: + var v = JsonSerializer.Deserialize(ref reader, options); + return new ReadOnlyCollection(new List(1) { v}); } + } - protected virtual bool ReadMore(ref Utf8JsonReader reader, JsonSerializerOptions options, string propertyName, TErrorCause errorCause) => false; - - public override void Write(Utf8JsonWriter writer, TErrorCause value, JsonSerializerOptions options) - { - writer.WriteStartObject(); + protected virtual bool ReadMore(ref Utf8JsonReader reader, JsonSerializerOptions options, string propertyName, TErrorCause errorCause) => false; - static void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, string name, T value) - { - if (value == null) return; + public override void Write(Utf8JsonWriter writer, TErrorCause value, JsonSerializerOptions options) + { + writer.WriteStartObject(); - writer.WritePropertyName(name); - JsonSerializer.Serialize(writer, value, options); - } + static void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, string name, T value) + { + if (value == null) return; - //Serialize(writer, options, "bytes_limit", value.BytesLimit); - //Serialize(writer, options, "bytes_wanted", value.BytesWanted); - Serialize(writer, options, "caused_by", value.CausedBy); - //Serialize(writer, options, "col", value.Column); - //Serialize(writer, options, "failed_shards", value.FailedShards); - //Serialize(writer, options, "grouped", value.Grouped); - Serialize(writer, options, "index", value.Index); - Serialize(writer, options, "index_uuid", value.IndexUUID); - //Serialize(writer, options, "lang", value.Language); - //Serialize(writer, options, "license.expired.feature", value.LicensedExpiredFeature); - //Serialize(writer, options, "line", value.Line); - //Serialize(writer, options, "phase", value.Phase); - //Serialize(writer, options, "reason", value.Reason); - //Serialize(writer, options, "resource.id", value.ResourceId); - //Serialize(writer, options, "resource.type", value.ResourceType); - //Serialize(writer, options, "script", value.Script); - //Serialize(writer, options, "script_stack", value.ScriptStack); - //Serialize(writer, options, "shard", value.Shard); - Serialize(writer, options, "stack_trace", value.StackTrace); - Serialize(writer, options, "type", value.Type); - - foreach (var kv in value.AdditionalProperties) - Serialize(writer, options, kv.Key, kv.Value); - writer.WriteEndObject(); + writer.WritePropertyName(name); + JsonSerializer.Serialize(writer, value, options); } + + //Serialize(writer, options, "bytes_limit", value.BytesLimit); + //Serialize(writer, options, "bytes_wanted", value.BytesWanted); + Serialize(writer, options, "caused_by", value.CausedBy); + //Serialize(writer, options, "col", value.Column); + //Serialize(writer, options, "failed_shards", value.FailedShards); + //Serialize(writer, options, "grouped", value.Grouped); + Serialize(writer, options, "index", value.Index); + Serialize(writer, options, "index_uuid", value.IndexUUID); + //Serialize(writer, options, "lang", value.Language); + //Serialize(writer, options, "license.expired.feature", value.LicensedExpiredFeature); + //Serialize(writer, options, "line", value.Line); + //Serialize(writer, options, "phase", value.Phase); + //Serialize(writer, options, "reason", value.Reason); + //Serialize(writer, options, "resource.id", value.ResourceId); + //Serialize(writer, options, "resource.type", value.ResourceType); + //Serialize(writer, options, "script", value.Script); + //Serialize(writer, options, "script_stack", value.ScriptStack); + //Serialize(writer, options, "shard", value.Shard); + Serialize(writer, options, "stack_trace", value.StackTrace); + Serialize(writer, options, "type", value.Type); + + foreach (var kv in value.AdditionalProperties) + Serialize(writer, options, kv.Key, kv.Value); + writer.WriteEndObject(); } } diff --git a/src/Elastic.Transport/Components/Serialization/Converters/ExceptionConverter.cs b/src/Elastic.Transport/Components/Serialization/Converters/ExceptionConverter.cs index 35160fe..a635f0a 100644 --- a/src/Elastic.Transport/Components/Serialization/Converters/ExceptionConverter.cs +++ b/src/Elastic.Transport/Components/Serialization/Converters/ExceptionConverter.cs @@ -11,113 +11,112 @@ using System.Text.Json.Serialization; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A Json Converter that serializes an exception by flattening it and +/// inner exceptions into an array of objects, including depth. +/// +internal class ExceptionConverter : JsonConverter { - /// - /// A Json Converter that serializes an exception by flattening it and - /// inner exceptions into an array of objects, including depth. - /// - internal class ExceptionConverter : JsonConverter + private static List> FlattenExceptions(Exception e) { - private static List> FlattenExceptions(Exception e) + var maxExceptions = 20; + var exceptions = new List>(maxExceptions); + var depth = 0; + do { - var maxExceptions = 20; - var exceptions = new List>(maxExceptions); - var depth = 0; - do - { - var o = ToDictionary(e, depth); - exceptions.Add(o); - depth++; - e = e.InnerException; - } while (depth < maxExceptions && e != null); + var o = ToDictionary(e, depth); + exceptions.Add(o); + depth++; + e = e.InnerException; + } while (depth < maxExceptions && e != null); - return exceptions; - } + return exceptions; + } - private static Dictionary ToDictionary(Exception e, int depth) - { - var o = new Dictionary(10); - var si = new SerializationInfo(e.GetType(), new FormatterConverter()); - var sc = new StreamingContext(); - e.GetObjectData(si, sc); - - var helpUrl = si.GetString("HelpURL"); - var stackTrace = si.GetString("StackTraceString"); - var remoteStackTrace = si.GetString("RemoteStackTraceString"); - var remoteStackIndex = si.GetInt32("RemoteStackIndex"); - var exceptionMethod = si.GetString("ExceptionMethod"); - var hresult = si.GetInt32("HResult"); - var source = si.GetString("Source"); - var className = si.GetString("ClassName"); - - o.Add("Depth", depth); - o.Add("ClassName", className); - o.Add("Message", e.Message); - o.Add("Source", source); - o.Add("StackTraceString", stackTrace); - o.Add("RemoteStackTraceString", remoteStackTrace); - o.Add("RemoteStackIndex", remoteStackIndex); - o.Add("HResult", hresult); - o.Add("HelpURL", helpUrl); - - WriteStructuredExceptionMethod(o, exceptionMethod); - return o; - } + private static Dictionary ToDictionary(Exception e, int depth) + { + var o = new Dictionary(10); + var si = new SerializationInfo(e.GetType(), new FormatterConverter()); + var sc = new StreamingContext(); + e.GetObjectData(si, sc); - private static void WriteStructuredExceptionMethod(Dictionary o, string exceptionMethodString) - { - if (string.IsNullOrWhiteSpace(exceptionMethodString)) return; + var helpUrl = si.GetString("HelpURL"); + var stackTrace = si.GetString("StackTraceString"); + var remoteStackTrace = si.GetString("RemoteStackTraceString"); + var remoteStackIndex = si.GetInt32("RemoteStackIndex"); + var exceptionMethod = si.GetString("ExceptionMethod"); + var hresult = si.GetInt32("HResult"); + var source = si.GetString("Source"); + var className = si.GetString("ClassName"); - var args = exceptionMethodString.Split('\0', '\n'); + o.Add("Depth", depth); + o.Add("ClassName", className); + o.Add("Message", e.Message); + o.Add("Source", source); + o.Add("StackTraceString", stackTrace); + o.Add("RemoteStackTraceString", remoteStackTrace); + o.Add("RemoteStackIndex", remoteStackIndex); + o.Add("HResult", hresult); + o.Add("HelpURL", helpUrl); - if (args.Length != 5) return; + WriteStructuredExceptionMethod(o, exceptionMethod); + return o; + } - var memberType = int.Parse(args[0], CultureInfo.InvariantCulture); - var name = args[1]; - var assemblyName = args[2]; - var className = args[3]; - var signature = args[4]; - var an = new AssemblyName(assemblyName); - var exceptionMethod = new Dictionary(7) - { - { "Name", name }, - { "AssemblyName", an.Name }, - { "AssemblyVersion", an.Version.ToString() }, - { "AssemblyCulture", an.CultureName }, - { "ClassName", className }, - { "Signature", signature }, - { "MemberType", memberType } - }; - - o.Add("ExceptionMethod", exceptionMethod); - } + private static void WriteStructuredExceptionMethod(Dictionary o, string exceptionMethodString) + { + if (string.IsNullOrWhiteSpace(exceptionMethodString)) return; - public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotSupportedException(); + var args = exceptionMethodString.Split('\0', '\n'); - public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + if (args.Length != 5) return; + + var memberType = int.Parse(args[0], CultureInfo.InvariantCulture); + var name = args[1]; + var assemblyName = args[2]; + var className = args[3]; + var signature = args[4]; + var an = new AssemblyName(assemblyName); + var exceptionMethod = new Dictionary(7) { - if (value == null) - { - writer.WriteNullValue(); - return; - } + { "Name", name }, + { "AssemblyName", an.Name }, + { "AssemblyVersion", an.Version.ToString() }, + { "AssemblyCulture", an.CultureName }, + { "ClassName", className }, + { "Signature", signature }, + { "MemberType", memberType } + }; + + o.Add("ExceptionMethod", exceptionMethod); + } + + public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); - var flattenedExceptions = FlattenExceptions(value); - writer.WriteStartArray(); - for (var i = 0; i < flattenedExceptions.Count; i++) + public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + var flattenedExceptions = FlattenExceptions(value); + writer.WriteStartArray(); + for (var i = 0; i < flattenedExceptions.Count; i++) + { + var flattenedException = flattenedExceptions[i]; + writer.WriteStartObject(); + foreach (var kv in flattenedException) { - var flattenedException = flattenedExceptions[i]; - writer.WriteStartObject(); - foreach (var kv in flattenedException) - { - writer.WritePropertyName(kv.Key); - JsonSerializer.Serialize(writer, kv.Value, options); - } - writer.WriteEndObject(); + writer.WritePropertyName(kv.Key); + JsonSerializer.Serialize(writer, kv.Value, options); } - writer.WriteEndArray(); + writer.WriteEndObject(); } + writer.WriteEndArray(); } } diff --git a/src/Elastic.Transport/Components/Serialization/DiagnosticsSerializerProxy.cs b/src/Elastic.Transport/Components/Serialization/DiagnosticsSerializerProxy.cs index dfaf27d..94a179c 100644 --- a/src/Elastic.Transport/Components/Serialization/DiagnosticsSerializerProxy.cs +++ b/src/Elastic.Transport/Components/Serialization/DiagnosticsSerializerProxy.cs @@ -9,75 +9,74 @@ using System.Threading.Tasks; using Elastic.Transport.Diagnostics; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Wraps configured serializer so that we can emit diagnostics per configured serializer. +/// +internal class DiagnosticsSerializerProxy : Serializer { + private readonly Serializer _serializer; + private readonly SerializerRegistrationInformation _state; + private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.Serializer.SourceName); + /// - /// Wraps configured serializer so that we can emit diagnostics per configured serializer. + /// /// - internal class DiagnosticsSerializerProxy : Serializer + /// The serializer we are proxying + /// + public DiagnosticsSerializerProxy(Serializer serializer, string purpose = "request/response") { - private readonly Serializer _serializer; - private readonly SerializerRegistrationInformation _state; - private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.Serializer.SourceName); - - /// - /// - /// - /// The serializer we are proxying - /// - public DiagnosticsSerializerProxy(Serializer serializer, string purpose = "request/response") - { - _serializer = serializer; - _state = new SerializerRegistrationInformation(serializer.GetType(), purpose); - } + _serializer = serializer; + _state = new SerializerRegistrationInformation(serializer.GetType(), purpose); + } - /// - /// Access to the inner wrapped by this proxy. - /// - public Serializer InnerSerializer => _serializer; + /// + /// Access to the inner wrapped by this proxy. + /// + public Serializer InnerSerializer => _serializer; - /// > - public override object Deserialize(Type type, Stream stream) - { - using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) - return _serializer.Deserialize(type, stream); - } + /// > + public override object Deserialize(Type type, Stream stream) + { + using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) + return _serializer.Deserialize(type, stream); + } - /// > - public override T Deserialize(Stream stream) - { - using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) - return _serializer.Deserialize(stream); - } + /// > + public override T Deserialize(Stream stream) + { + using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) + return _serializer.Deserialize(stream); + } - /// > - public override ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) - { - using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) - return _serializer.DeserializeAsync(type, stream, cancellationToken); - } + /// > + public override ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) + { + using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) + return _serializer.DeserializeAsync(type, stream, cancellationToken); + } - /// > - public override ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) - { - using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) - return _serializer.DeserializeAsync(stream, cancellationToken); - } + /// > + public override ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) + { + using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Deserialize, _state)) + return _serializer.DeserializeAsync(stream, cancellationToken); + } - /// > - public override void Serialize(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None) - { - using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Serialize, _state)) - _serializer.Serialize(data, stream, formatting); - } + /// > + public override void Serialize(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None) + { + using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Serialize, _state)) + _serializer.Serialize(data, stream, formatting); + } - /// > - public override Task SerializeAsync(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None, - CancellationToken cancellationToken = default - ) - { - using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Serialize, _state)) - return _serializer.SerializeAsync(data, stream, formatting, cancellationToken); - } + /// > + public override Task SerializeAsync(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None, + CancellationToken cancellationToken = default + ) + { + using (DiagnosticSource.Diagnose(DiagnosticSources.Serializer.Serialize, _state)) + return _serializer.SerializeAsync(data, stream, formatting, cancellationToken); } } diff --git a/src/Elastic.Transport/Components/Serialization/JsonElementExtensions.cs b/src/Elastic.Transport/Components/Serialization/JsonElementExtensions.cs index b6058ba..0737900 100644 --- a/src/Elastic.Transport/Components/Serialization/JsonElementExtensions.cs +++ b/src/Elastic.Transport/Components/Serialization/JsonElementExtensions.cs @@ -7,32 +7,31 @@ using System.Linq; using System.Text.Json; -namespace Elastic.Transport.Extensions +namespace Elastic.Transport.Extensions; + +internal static class JsonElementExtensions { - internal static class JsonElementExtensions - { - /// - /// Fully consumes a json element representing a json object. Meaning it will attempt to unwrap all JsonElement values - /// recursively to their actual types. This should only be used in the context of which is - /// allowed to be slow yet convenient - /// - public static IDictionary ToDictionary(this JsonElement e) => - e.ValueKind switch - { - JsonValueKind.Object => e.EnumerateObject() - .Aggregate(new Dictionary(), (dict, je) => - { - dict.Add(je.Name, DynamicValue.ConsumeJsonElement(typeof(object), je.Value)); - return dict; - }), - JsonValueKind.Array => e.EnumerateArray() - .Select((je, i) => (i, o: DynamicValue.ConsumeJsonElement(typeof(object), je))) - .Aggregate(new Dictionary(), (dict, t) => - { - dict.Add(t.i.ToString(CultureInfo.InvariantCulture), t.o); - return dict; - }), - _ => null - }; - } + /// + /// Fully consumes a json element representing a json object. Meaning it will attempt to unwrap all JsonElement values + /// recursively to their actual types. This should only be used in the context of which is + /// allowed to be slow yet convenient + /// + public static IDictionary ToDictionary(this JsonElement e) => + e.ValueKind switch + { + JsonValueKind.Object => e.EnumerateObject() + .Aggregate(new Dictionary(), (dict, je) => + { + dict.Add(je.Name, DynamicValue.ConsumeJsonElement(typeof(object), je.Value)); + return dict; + }), + JsonValueKind.Array => e.EnumerateArray() + .Select((je, i) => (i, o: DynamicValue.ConsumeJsonElement(typeof(object), je))) + .Aggregate(new Dictionary(), (dict, t) => + { + dict.Add(t.i.ToString(CultureInfo.InvariantCulture), t.o); + return dict; + }), + _ => null + }; } diff --git a/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs b/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs index fc46592..6c53bcf 100644 --- a/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs +++ b/src/Elastic.Transport/Components/Serialization/LowLevelRequestResponseSerializer.cs @@ -21,7 +21,7 @@ namespace Elastic.Transport; /// public class LowLevelRequestResponseSerializer : Serializer { - //TODO explore removing this or make internal, this provides a path that circumvents the configured ITransportSerializer + //TODO explore removing this or make internal, this provides a path that circumvents the configured HttpTransportSerializer /// Provides a static reusable reference to an instance of to promote reuse public static readonly LowLevelRequestResponseSerializer Instance = new(); diff --git a/src/Elastic.Transport/Components/Serialization/SerializationFormatting.cs b/src/Elastic.Transport/Components/Serialization/SerializationFormatting.cs index 039d148..e438bb9 100644 --- a/src/Elastic.Transport/Components/Serialization/SerializationFormatting.cs +++ b/src/Elastic.Transport/Components/Serialization/SerializationFormatting.cs @@ -2,24 +2,23 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A hint to how to format the json. +/// Implementation of might choose to ignore this hint though. +/// +public enum SerializationFormatting { /// - /// A hint to how to format the json. - /// Implementation of might choose to ignore this hint though. + /// Serializer should not render the json with whitespace and line endings. + /// implementation HAVE to be able to adhere this value as for instance nd-json relies on this /// - public enum SerializationFormatting - { - /// - /// Serializer should not render the json with whitespace and line endings. - /// implementation HAVE to be able to adhere this value as for instance nd-json relies on this - /// - None, + None, - /// - /// A hint that the user prefers readable data being written. implementations - /// should try to adhere to this but won't break anything if they don't. - /// - Indented - } + /// + /// A hint that the user prefers readable data being written. implementations + /// should try to adhere to this but won't break anything if they don't. + /// + Indented } diff --git a/src/Elastic.Transport/Components/Serialization/Serializer.cs b/src/Elastic.Transport/Components/Serialization/Serializer.cs index 3607764..fa996fc 100644 --- a/src/Elastic.Transport/Components/Serialization/Serializer.cs +++ b/src/Elastic.Transport/Components/Serialization/Serializer.cs @@ -7,48 +7,47 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// When the needs to (de)serialize anything it will call into the +/// implementation of this base class. +/// +/// e.g: Whenever the receives +/// to serialize that data. +/// +public abstract class Serializer { + // TODO: Overloads taking a Memory/Span?? + + /// Deserialize to an instance of + public abstract object Deserialize(Type type, Stream stream); + + /// Deserialize to an instance of + public abstract T Deserialize(Stream stream); + + /// + public abstract ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default); + + /// + public abstract ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default); + /// - /// When the needs to (de)serialize anything it will call into the - /// implementation of this base class. - /// - /// e.g: Whenever the receives - /// to serialize that data. + /// Serialize an instance of to using . /// - public abstract class Serializer - { - // TODO: Overloads taking a Memory/Span?? - - /// Deserialize to an instance of - public abstract object Deserialize(Type type, Stream stream); - - /// Deserialize to an instance of - public abstract T Deserialize(Stream stream); - - /// - public abstract ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default); - - /// - public abstract ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default); - - /// - /// Serialize an instance of to using . - /// - /// The instance of that we want to serialize. - /// The stream to serialize to. - /// - /// Formatting hint. Note that not all implementations of are able to - /// satisfy this hint, including the default serializer that is shipped with 8.0. - /// - public abstract void Serialize(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None); - - /// - public abstract Task SerializeAsync( - T data, - Stream stream, - SerializationFormatting formatting = SerializationFormatting.None, - CancellationToken cancellationToken = default - ); - } + /// The instance of that we want to serialize. + /// The stream to serialize to. + /// + /// Formatting hint. Note that not all implementations of are able to + /// satisfy this hint, including the default serializer that is shipped with 8.0. + /// + public abstract void Serialize(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None); + + /// + public abstract Task SerializeAsync( + T data, + Stream stream, + SerializationFormatting formatting = SerializationFormatting.None, + CancellationToken cancellationToken = default + ); } diff --git a/src/Elastic.Transport/Components/Serialization/SerializerRegistrationInformation.cs b/src/Elastic.Transport/Components/Serialization/SerializerRegistrationInformation.cs index e7ddd99..96fb63d 100644 --- a/src/Elastic.Transport/Components/Serialization/SerializerRegistrationInformation.cs +++ b/src/Elastic.Transport/Components/Serialization/SerializerRegistrationInformation.cs @@ -4,34 +4,33 @@ using System; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// Provides some information to the transport auditing and diagnostics infrastructure about the serializer in use and its +public sealed class SerializerRegistrationInformation { - /// Provides some information to the transport auditing and diagnostics infrastructure about the serializer in use and its - public sealed class SerializerRegistrationInformation - { - private readonly string _stringRepresentation; + private readonly string _stringRepresentation; - /// - public SerializerRegistrationInformation(Type type, string purpose) - { - TypeInformation = type; - Purpose = purpose; - _stringRepresentation = $"{Purpose}: {TypeInformation.FullName}"; - } + /// + public SerializerRegistrationInformation(Type type, string purpose) + { + TypeInformation = type; + Purpose = purpose; + _stringRepresentation = $"{Purpose}: {TypeInformation.FullName}"; + } - /// The type of in use currently - // ReSharper disable once MemberCanBePrivate.Global - public Type TypeInformation { get; } + /// The type of in use currently + // ReSharper disable once MemberCanBePrivate.Global + public Type TypeInformation { get; } - /// - /// A string describing the purpose of the serializer emitting this events. - /// In `Elastisearch.Net` this will always be "request/response" - /// Using `Nest` this could also be `source` allowing you to differentiate between the internal and configured source serializer - /// - // ReSharper disable once MemberCanBePrivate.Global - public string Purpose { get; } + /// + /// A string describing the purpose of the serializer emitting this events. + /// In `Elastisearch.Net` this will always be "request/response" + /// Using `Nest` this could also be `source` allowing you to differentiate between the internal and configured source serializer + /// + // ReSharper disable once MemberCanBePrivate.Global + public string Purpose { get; } - /// A precalculated string representation of the serializer in use - public override string ToString() => _stringRepresentation; - } + /// A precalculated string representation of the serializer in use + public override string ToString() => _stringRepresentation; } diff --git a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs index c78fc3f..d3b215d 100644 --- a/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs +++ b/src/Elastic.Transport/Components/Serialization/TransportSerializerExtensions.cs @@ -2,79 +2,78 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport.Extensions +namespace Elastic.Transport.Extensions; + +/// +/// A set of handy extension methods for +/// +public static class TransportSerializerExtensions { /// - /// A set of handy extension methods for + /// Extension method that serializes an instance of to a byte array. /// - public static class TransportSerializerExtensions - { - /// - /// Extension method that serializes an instance of to a byte array. - /// - public static byte[] SerializeToBytes( - this Serializer serializer, - T data, - SerializationFormatting formatting = SerializationFormatting.None) => - SerializeToBytes(serializer, data, TransportConfiguration.DefaultMemoryStreamFactory, formatting); + public static byte[] SerializeToBytes( + this Serializer serializer, + T data, + SerializationFormatting formatting = SerializationFormatting.None) => + SerializeToBytes(serializer, data, TransportConfiguration.DefaultMemoryStreamFactory, formatting); - /// - /// Extension method that serializes an instance of to a byte array. - /// - /// - /// - /// A factory yielding MemoryStream instances, defaults to - /// that yields memory streams backed by pooled byte arrays. - /// - /// - /// - public static byte[] SerializeToBytes( - this Serializer serializer, - T data, - IMemoryStreamFactory memoryStreamFactory, - SerializationFormatting formatting = SerializationFormatting.None - ) + /// + /// Extension method that serializes an instance of to a byte array. + /// + /// + /// + /// A factory yielding MemoryStream instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// + /// + public static byte[] SerializeToBytes( + this Serializer serializer, + T data, + MemoryStreamFactory memoryStreamFactory, + SerializationFormatting formatting = SerializationFormatting.None + ) + { + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using (var ms = memoryStreamFactory.Create()) { - memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using (var ms = memoryStreamFactory.Create()) - { - serializer.Serialize(data, ms, formatting); - return ms.ToArray(); - } + serializer.Serialize(data, ms, formatting); + return ms.ToArray(); } + } - /// - /// Extension method that serializes an instance of to a string. - /// - public static string SerializeToString( - this Serializer serializer, - T data, - SerializationFormatting formatting = SerializationFormatting.None) => - SerializeToString(serializer, data, TransportConfiguration.DefaultMemoryStreamFactory, formatting); + /// + /// Extension method that serializes an instance of to a string. + /// + public static string SerializeToString( + this Serializer serializer, + T data, + SerializationFormatting formatting = SerializationFormatting.None) => + SerializeToString(serializer, data, TransportConfiguration.DefaultMemoryStreamFactory, formatting); - /// - /// Extension method that serializes an instance of to a string. - /// - /// - /// - /// A factory yielding MemoryStream instances, defaults to - /// that yields memory streams backed by pooled byte arrays. - /// - /// - /// - public static string SerializeToString( - this Serializer serializer, - T data, - IMemoryStreamFactory memoryStreamFactory, - SerializationFormatting formatting = SerializationFormatting.None - ) + /// + /// Extension method that serializes an instance of to a string. + /// + /// + /// + /// A factory yielding MemoryStream instances, defaults to + /// that yields memory streams backed by pooled byte arrays. + /// + /// + /// + public static string SerializeToString( + this Serializer serializer, + T data, + MemoryStreamFactory memoryStreamFactory, + SerializationFormatting formatting = SerializationFormatting.None + ) + { + memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; + using (var ms = memoryStreamFactory.Create()) { - memoryStreamFactory ??= TransportConfiguration.DefaultMemoryStreamFactory; - using (var ms = memoryStreamFactory.Create()) - { - serializer.Serialize(data, ms, formatting); - return ms.Utf8String(); - } + serializer.Serialize(data, ms, formatting); + return ms.Utf8String(); } } } diff --git a/src/Elastic.Transport/Components/TransportClient/CertificateHelpers.cs b/src/Elastic.Transport/Components/TransportClient/CertificateHelpers.cs index c9c4025..7159937 100644 --- a/src/Elastic.Transport/Components/TransportClient/CertificateHelpers.cs +++ b/src/Elastic.Transport/Components/TransportClient/CertificateHelpers.cs @@ -6,47 +6,46 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -namespace Elastic.Transport +namespace Elastic.Transport; + +internal static class CertificateHelpers { - internal static class CertificateHelpers - { - private const string _colon = ":"; - private const string _hyphen = "-"; + private const string _colon = ":"; + private const string _hyphen = "-"; - /// - /// Returns the result of validating the fingerprint of a certificate against an expected fingerprint string. - /// - public static bool ValidateCertificateFingerprint(X509Certificate certificate, string expectedFingerprint) - { - string sha256Fingerprint; + /// + /// Returns the result of validating the fingerprint of a certificate against an expected fingerprint string. + /// + public static bool ValidateCertificateFingerprint(X509Certificate certificate, string expectedFingerprint) + { + string sha256Fingerprint; #if DOTNETCORE && !NETSTANDARD2_0 - sha256Fingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256); + sha256Fingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256); #else - using var alg = SHA256.Create(); + using var alg = SHA256.Create(); - var sha256FingerprintBytes = alg.ComputeHash(certificate.GetRawCertData()); - sha256Fingerprint = BitConverter.ToString(sha256FingerprintBytes); + var sha256FingerprintBytes = alg.ComputeHash(certificate.GetRawCertData()); + sha256Fingerprint = BitConverter.ToString(sha256FingerprintBytes); #endif - sha256Fingerprint = ComparableFingerprint(sha256Fingerprint); - return expectedFingerprint.Equals(sha256Fingerprint, StringComparison.OrdinalIgnoreCase); - } - - /// - /// Cleans the fingerprint by removing colons and dashes so that a comparison can be made - /// without such characters affecting the result. - /// - public static string ComparableFingerprint(string fingerprint) - { - var finalFingerprint = fingerprint; - - if (fingerprint.Contains(_colon)) - finalFingerprint = fingerprint.Replace(_colon, string.Empty); - else if (fingerprint.Contains(_hyphen)) - finalFingerprint = fingerprint.Replace(_hyphen, string.Empty); - - return finalFingerprint; - } + sha256Fingerprint = ComparableFingerprint(sha256Fingerprint); + return expectedFingerprint.Equals(sha256Fingerprint, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Cleans the fingerprint by removing colons and dashes so that a comparison can be made + /// without such characters affecting the result. + /// + public static string ComparableFingerprint(string fingerprint) + { + var finalFingerprint = fingerprint; + + if (fingerprint.Contains(_colon)) + finalFingerprint = fingerprint.Replace(_colon, string.Empty); + else if (fingerprint.Contains(_hyphen)) + finalFingerprint = fingerprint.Replace(_hyphen, string.Empty); + + return finalFingerprint; } } diff --git a/src/Elastic.Transport/Components/TransportClient/CertificateValidations.cs b/src/Elastic.Transport/Components/TransportClient/CertificateValidations.cs index 47e5fa1..e153046 100644 --- a/src/Elastic.Transport/Components/TransportClient/CertificateValidations.cs +++ b/src/Elastic.Transport/Components/TransportClient/CertificateValidations.cs @@ -6,125 +6,124 @@ using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A collection of handy baked in server certificate validation callbacks +/// +public static class CertificateValidations { /// - /// A collection of handy baked in server certificate validation callbacks + /// DANGEROUS, never use this in production validates ALL certificates to true. + /// + /// Always true, allowing ALL certificates + public static bool AllowAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => true; + + /// + /// Always false, in effect blocking ALL certificates + /// + /// Always false, always blocking ALL certificates + public static bool DenyAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => false; + + /// + /// Helper to create a certificate validation callback based on the certificate authority certificate that we used to + /// generate the nodes certificates with. This callback expects the CA to be part of the chain as intermediate CA. + /// + /// The ca certificate used to generate the nodes certificate + /// + /// Custom CA are never trusted by default unless they are in the machines trusted store, set this to true + /// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted. + /// + /// By default we do not check revocation, it is however recommended to check this (either offline or online). + public static Func AuthorityPartOfChain( + X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck + ) => + (sender, cert, chain, errors) => + errors == SslPolicyErrors.None + || ValidIntermediateCa(caCertificate, cert, chain, trustRoot, revocationMode); + + /// + /// Helper to create a certificate validation callback based on the certificate authority certificate that we used to + /// generate the nodes certificates with. This callback does NOT expect the CA to be part of the chain presented by the server. + /// Including the root certificate in the chain increases the SSL handshake size. + /// Elasticsearch's certgen by default does not include the CA in the certificate chain. /// - public static class CertificateValidations + /// The ca certificate used to generate the nodes certificate + /// + /// Custom CA are never trusted by default unless they are in the machines trusted store, set this to true + /// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted. + /// + /// By default we do not check revocation, it is however recommended to check this (either offline or online). + public static Func AuthorityIsRoot( + X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck + ) => + (sender, cert, chain, errors) => + errors == SslPolicyErrors.None + || ValidRootCa(caCertificate, cert, trustRoot, revocationMode); + + private static bool ValidRootCa(X509Certificate caCertificate, X509Certificate certificate, bool trustRoot, + X509RevocationMode revocationMode + ) { - /// - /// DANGEROUS, never use this in production validates ALL certificates to true. - /// - /// Always true, allowing ALL certificates - public static bool AllowAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => true; - - /// - /// Always false, in effect blocking ALL certificates - /// - /// Always false, always blocking ALL certificates - public static bool DenyAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => false; - - /// - /// Helper to create a certificate validation callback based on the certificate authority certificate that we used to - /// generate the nodes certificates with. This callback expects the CA to be part of the chain as intermediate CA. - /// - /// The ca certificate used to generate the nodes certificate - /// - /// Custom CA are never trusted by default unless they are in the machines trusted store, set this to true - /// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted. - /// - /// By default we do not check revocation, it is however recommended to check this (either offline or online). - public static Func AuthorityPartOfChain( - X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck - ) => - (sender, cert, chain, errors) => - errors == SslPolicyErrors.None - || ValidIntermediateCa(caCertificate, cert, chain, trustRoot, revocationMode); - - /// - /// Helper to create a certificate validation callback based on the certificate authority certificate that we used to - /// generate the nodes certificates with. This callback does NOT expect the CA to be part of the chain presented by the server. - /// Including the root certificate in the chain increases the SSL handshake size. - /// Elasticsearch's certgen by default does not include the CA in the certificate chain. - /// - /// The ca certificate used to generate the nodes certificate - /// - /// Custom CA are never trusted by default unless they are in the machines trusted store, set this to true - /// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted. - /// - /// By default we do not check revocation, it is however recommended to check this (either offline or online). - public static Func AuthorityIsRoot( - X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck - ) => - (sender, cert, chain, errors) => - errors == SslPolicyErrors.None - || ValidRootCa(caCertificate, cert, trustRoot, revocationMode); - - private static bool ValidRootCa(X509Certificate caCertificate, X509Certificate certificate, bool trustRoot, - X509RevocationMode revocationMode - ) + var ca = new X509Certificate2(caCertificate); + var privateChain = new X509Chain { ChainPolicy = { RevocationMode = revocationMode } }; + privateChain.ChainPolicy.ExtraStore.Add(ca); + privateChain.Build(new X509Certificate2(certificate)); + + //lets validate the our chain status + foreach (var chainStatus in privateChain.ChainStatus) { - var ca = new X509Certificate2(caCertificate); - var privateChain = new X509Chain { ChainPolicy = { RevocationMode = revocationMode } }; - privateChain.ChainPolicy.ExtraStore.Add(ca); - privateChain.Build(new X509Certificate2(certificate)); - - //lets validate the our chain status - foreach (var chainStatus in privateChain.ChainStatus) - { - //custom CA's that are not in the machine trusted store will always have this status - //by setting trustRoot = true (default) we skip this error - if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue; - - //trustRoot is false so we expected our CA to be in the machines trusted store - if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false; - - //otherwise if the chain has any error of any sort return false - if (chainStatus.Status != X509ChainStatusFlags.NoError) return false; - } - return true; + //custom CA's that are not in the machine trusted store will always have this status + //by setting trustRoot = true (default) we skip this error + if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue; + + //trustRoot is false so we expected our CA to be in the machines trusted store + if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false; + + //otherwise if the chain has any error of any sort return false + if (chainStatus.Status != X509ChainStatusFlags.NoError) return false; } + return true; + } + + private static bool ValidIntermediateCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot, + X509RevocationMode revocationMode + ) + { + var ca = new X509Certificate2(caCertificate); + var privateChain = new X509Chain { ChainPolicy = { RevocationMode = revocationMode } }; + privateChain.ChainPolicy.ExtraStore.Add(ca); + privateChain.Build(new X509Certificate2(certificate)); + + //Assert our chain has the same number of elements as the certifcate presented by the server + if (chain.ChainElements.Count != privateChain.ChainElements.Count) return false; - private static bool ValidIntermediateCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot, - X509RevocationMode revocationMode - ) + //lets validate the our chain status + foreach (var chainStatus in privateChain.ChainStatus) { - var ca = new X509Certificate2(caCertificate); - var privateChain = new X509Chain { ChainPolicy = { RevocationMode = revocationMode } }; - privateChain.ChainPolicy.ExtraStore.Add(ca); - privateChain.Build(new X509Certificate2(certificate)); - - //Assert our chain has the same number of elements as the certifcate presented by the server - if (chain.ChainElements.Count != privateChain.ChainElements.Count) return false; - - //lets validate the our chain status - foreach (var chainStatus in privateChain.ChainStatus) - { - //custom CA's that are not in the machine trusted store will always have this status - //by setting trustRoot = true (default) we skip this error - if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue; - - //trustRoot is false so we expected our CA to be in the machines trusted store - if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false; - - //otherwise if the chain has any error of any sort return false - if (chainStatus.Status != X509ChainStatusFlags.NoError) return false; - } - - var found = false; - //We are going to walk both chains and make sure the thumbprints align - //while making sure one of the chains certificates presented by the server has our expected CA thumbprint - for (var i = 0; i < chain.ChainElements.Count; i++) - { - var c = chain.ChainElements[i].Certificate.Thumbprint; - var cPrivate = privateChain.ChainElements[i].Certificate.Thumbprint; - if (!found && c == ca.Thumbprint) found = true; - - //mis aligned certificate chain, return false so we do not accept this certificate - if (c != cPrivate) return false; - } - return found; + //custom CA's that are not in the machine trusted store will always have this status + //by setting trustRoot = true (default) we skip this error + if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue; + + //trustRoot is false so we expected our CA to be in the machines trusted store + if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false; + + //otherwise if the chain has any error of any sort return false + if (chainStatus.Status != X509ChainStatusFlags.NoError) return false; + } + + var found = false; + //We are going to walk both chains and make sure the thumbprints align + //while making sure one of the chains certificates presented by the server has our expected CA thumbprint + for (var i = 0; i < chain.ChainElements.Count; i++) + { + var c = chain.ChainElements[i].Certificate.Thumbprint; + var cPrivate = privateChain.ChainElements[i].Certificate.Thumbprint; + if (!found && c == ca.Thumbprint) found = true; + + //mis aligned certificate chain, return false so we do not accept this certificate + if (c != cPrivate) return false; } + return found; } } diff --git a/src/Elastic.Transport/Components/TransportClient/Content/RequestDataContent.cs b/src/Elastic.Transport/Components/TransportClient/Content/RequestDataContent.cs index bdee4f6..7631c33 100644 --- a/src/Elastic.Transport/Components/TransportClient/Content/RequestDataContent.cs +++ b/src/Elastic.Transport/Components/TransportClient/Content/RequestDataContent.cs @@ -19,212 +19,214 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Provides an implementation that exposes an output +/// which can be written to directly. The ability to push data to the output stream differs from the +/// where data is pulled and not pushed. +/// +internal sealed class RequestDataContent : HttpContent { - /// - /// Provides an implementation that exposes an output - /// which can be written to directly. The ability to push data to the output stream differs from the - /// where data is pulled and not pushed. - /// - internal class RequestDataContent : HttpContent - { - private readonly RequestData _requestData; + private readonly RequestData _requestData; - private readonly Func - _onStreamAvailableAsync; + private readonly Func + _onStreamAvailableAsync; - private readonly Action _onStreamAvailable; - private readonly CancellationToken _token; + private readonly Action _onStreamAvailable; + private readonly CancellationToken _token; - /// Constructor used in synchronous paths. - public RequestDataContent(RequestData requestData) - { - _requestData = requestData; - _token = default; - Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); - if (requestData.HttpCompression) - Headers.ContentEncoding.Add("gzip"); - - _onStreamAvailable = OnStreamAvailable; - _onStreamAvailableAsync = OnStreamAvailableAsync; - } + /// Constructor used in synchronous paths. + public RequestDataContent(RequestData requestData) + { + _requestData = requestData; + _token = default; + Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); + if (requestData.HttpCompression) + Headers.ContentEncoding.Add("gzip"); + + _onStreamAvailable = OnStreamAvailable; + _onStreamAvailableAsync = OnStreamAvailableAsync; + } - private static void OnStreamAvailable(RequestData data, Stream stream, HttpContent content, TransportContext context) - { - if (data.HttpCompression) stream = new GZipStream(stream, CompressionMode.Compress, false); + private static void OnStreamAvailable(RequestData data, Stream stream, HttpContent content, TransportContext context) + { + if (data.HttpCompression) stream = new GZipStream(stream, CompressionMode.Compress, false); - using (stream) data.PostData.Write(stream, data.ConnectionSettings); - } + using (stream) data.PostData.Write(stream, data.ConnectionSettings); + } - /// Constructor used in asynchronous paths. - public RequestDataContent(RequestData requestData, CancellationToken token) - { - _requestData = requestData; - _token = token; - Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); - if (requestData.HttpCompression) - Headers.ContentEncoding.Add("gzip"); - - _onStreamAvailable = OnStreamAvailable; - _onStreamAvailableAsync = OnStreamAvailableAsync; - } + /// Constructor used in asynchronous paths. + public RequestDataContent(RequestData requestData, CancellationToken token) + { + _requestData = requestData; + _token = token; + Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); + if (requestData.HttpCompression) + Headers.ContentEncoding.Add("gzip"); + + _onStreamAvailable = OnStreamAvailable; + _onStreamAvailableAsync = OnStreamAvailableAsync; + } - private static async Task OnStreamAvailableAsync(RequestData data, Stream stream, HttpContent content, TransportContext context, CancellationToken ctx = default) - { - if (data.HttpCompression) stream = new GZipStream(stream, CompressionMode.Compress, false); + private static async Task OnStreamAvailableAsync(RequestData data, Stream stream, HttpContent content, TransportContext context, CancellationToken ctx = default) + { + if (data.HttpCompression) stream = new GZipStream(stream, CompressionMode.Compress, false); #if NET5_0_OR_GREATER - await + await using (stream.ConfigureAwait(false)) +#else + using (stream) #endif - using (stream) - await data.PostData.WriteAsync(stream, data.ConnectionSettings, ctx).ConfigureAwait(false); - } + await data.PostData.WriteAsync(stream, data.ConnectionSettings, ctx).ConfigureAwait(false); + } + + /// + /// When this method is called, it calls the action provided in the constructor with the output + /// stream to write to. Once the action has completed its work it closes the stream which will + /// close this content instance and complete the HTTP request or response. + /// + /// The to which to write. + /// The associated . + /// A instance that is asynchronously serializing the object's content. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")] + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => + SerializeToStreamAsync(stream, context, default); + - /// - /// When this method is called, it calls the action provided in the constructor with the output - /// stream to write to. Once the action has completed its work it closes the stream which will - /// close this content instance and complete the HTTP request or response. - /// - /// The to which to write. - /// The associated . - /// A instance that is asynchronously serializing the object's content. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")] - protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => - SerializeToStreamAsync(stream, context, default); - - protected #if NET5_0_OR_GREATER - override + protected override +#else + private #endif - async Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) - { - var source = CancellationTokenSource.CreateLinkedTokenSource(_token, cancellationToken); - var serializeToStreamTask = new TaskCompletionSource(); - var wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); - await _onStreamAvailableAsync(_requestData, wrappedStream, this, context, source.Token).ConfigureAwait(false); - await serializeToStreamTask.Task.ConfigureAwait(false); - } + async Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) + { + var source = CancellationTokenSource.CreateLinkedTokenSource(_token, cancellationToken); + var serializeToStreamTask = new TaskCompletionSource(); + var wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); + await _onStreamAvailableAsync(_requestData, wrappedStream, this, context, source.Token).ConfigureAwait(false); + await serializeToStreamTask.Task.ConfigureAwait(false); + } #if NET5_0_OR_GREATER - protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken _) - { - var serializeToStreamTask = new TaskCompletionSource(); - using var wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); - _onStreamAvailable(_requestData, wrappedStream, this, context); - //await serializeToStreamTask.Task.ConfigureAwait(false); - } + protected override void SerializeToStream(Stream stream, TransportContext context, CancellationToken _) + { + var serializeToStreamTask = new TaskCompletionSource(); + using var wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); + _onStreamAvailable(_requestData, wrappedStream, this, context); + //await serializeToStreamTask.Task.ConfigureAwait(false); + } #endif - /// - /// Computes the length of the stream if possible. - /// - /// The computed length of the stream. - /// true if the length has been computed; otherwise false. - protected override bool TryComputeLength(out long length) + /// + /// Computes the length of the stream if possible. + /// + /// The computed length of the stream. + /// true if the length has been computed; otherwise false. + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } + + internal class CompleteTaskOnCloseStream : DelegatingStream + { + private readonly TaskCompletionSource _serializeToStreamTask; + + public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask) + : base(innerStream) { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; + Contract.Assert(serializeToStreamTask != null); + _serializeToStreamTask = serializeToStreamTask; } - internal class CompleteTaskOnCloseStream : DelegatingStream + protected override void Dispose(bool disposing) { - private readonly TaskCompletionSource _serializeToStreamTask; - - public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask) - : base(innerStream) - { - Contract.Assert(serializeToStreamTask != null); - _serializeToStreamTask = serializeToStreamTask; - } - - protected override void Dispose(bool disposing) - { - _serializeToStreamTask.TrySetResult(true); - base.Dispose(); - } - - public override void Close() => _serializeToStreamTask.TrySetResult(true); + _serializeToStreamTask.TrySetResult(true); + base.Dispose(); } - /// - /// Stream that delegates to inner stream. - /// This is taken from System.Net.Http - /// - internal abstract class DelegatingStream : Stream - { - private readonly Stream _innerStream; + public override void Close() => _serializeToStreamTask.TrySetResult(true); + } - protected DelegatingStream(Stream innerStream) => _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); + /// + /// Stream that delegates to inner stream. + /// This is taken from System.Net.Http + /// + internal abstract class DelegatingStream : Stream + { + private readonly Stream _innerStream; - public override bool CanRead => _innerStream.CanRead; + protected DelegatingStream(Stream innerStream) => _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); - public override bool CanSeek => _innerStream.CanSeek; + public override bool CanRead => _innerStream.CanRead; - public override bool CanWrite => _innerStream.CanWrite; + public override bool CanSeek => _innerStream.CanSeek; - public override long Length => _innerStream.Length; + public override bool CanWrite => _innerStream.CanWrite; - public override long Position - { - get => _innerStream.Position; - set => _innerStream.Position = value; - } + public override long Length => _innerStream.Length; - public override int ReadTimeout - { - get => _innerStream.ReadTimeout; - set => _innerStream.ReadTimeout = value; - } + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } - public override bool CanTimeout => _innerStream.CanTimeout; + public override int ReadTimeout + { + get => _innerStream.ReadTimeout; + set => _innerStream.ReadTimeout = value; + } - public override int WriteTimeout - { - get => _innerStream.WriteTimeout; - set => _innerStream.WriteTimeout = value; - } + public override bool CanTimeout => _innerStream.CanTimeout; - protected override void Dispose(bool disposing) - { - if (disposing) _innerStream.Dispose(); - base.Dispose(disposing); - } + public override int WriteTimeout + { + get => _innerStream.WriteTimeout; + set => _innerStream.WriteTimeout = value; + } + + protected override void Dispose(bool disposing) + { + if (disposing) _innerStream.Dispose(); + base.Dispose(disposing); + } - public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); + public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); - public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => - _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + _innerStream.ReadAsync(buffer, offset, count, cancellationToken); - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => - _innerStream.BeginRead(buffer, offset, count, callback, state); + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => + _innerStream.BeginRead(buffer, offset, count, callback, state); - public override int EndRead(IAsyncResult asyncResult) => _innerStream.EndRead(asyncResult); + public override int EndRead(IAsyncResult asyncResult) => _innerStream.EndRead(asyncResult); - public override int ReadByte() => _innerStream.ReadByte(); + public override int ReadByte() => _innerStream.ReadByte(); - public override void Flush() => _innerStream.Flush(); + public override void Flush() => _innerStream.Flush(); - public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken); + public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken); - public override void SetLength(long value) => _innerStream.SetLength(value); + public override void SetLength(long value) => _innerStream.SetLength(value); - public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => - _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + _innerStream.WriteAsync(buffer, offset, count, cancellationToken); - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => - _innerStream.BeginWrite(buffer, offset, count, callback, state); + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => + _innerStream.BeginWrite(buffer, offset, count, callback, state); - public override void EndWrite(IAsyncResult asyncResult) => _innerStream.EndWrite(asyncResult); + public override void EndWrite(IAsyncResult asyncResult) => _innerStream.EndWrite(asyncResult); - public override void WriteByte(byte value) => _innerStream.WriteByte(value); + public override void WriteByte(byte value) => _innerStream.WriteByte(value); - public override void Close() => _innerStream.Close(); - } + public override void Close() => _innerStream.Close(); } } #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ActiveHandlerTrackingEntry.cs b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ActiveHandlerTrackingEntry.cs index 5a24f7f..5fe81e8 100644 --- a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ActiveHandlerTrackingEntry.cs +++ b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ActiveHandlerTrackingEntry.cs @@ -11,14 +11,14 @@ using System.Diagnostics; using System.Threading; -namespace Elastic.Transport -{ - /// +namespace Elastic.Transport; + +/// /// Thread-safety: We treat this class as immutable except for the timer. Creating a new object - /// for the 'expiry' pool simplifies the threading requirements significantly. - /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/ActiveHandlerTrackingEntry.cs - /// - internal class ActiveHandlerTrackingEntry +/// for the 'expiry' pool simplifies the threading requirements significantly. +/// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/ActiveHandlerTrackingEntry.cs +/// + internal sealed class ActiveHandlerTrackingEntry { private static readonly TimerCallback TimerCallback = (s) => ((ActiveHandlerTrackingEntry)s).Timer_Tick(); private readonly object _lock; @@ -74,15 +74,14 @@ private void Timer_Tick() Debug.Assert(_timer != null); lock (_lock) - { - if (_timer == null) return; + { + if (_timer == null) return; - _timer.Dispose(); - _timer = null; + _timer.Dispose(); + _timer = null; - _callback(this); - } + _callback(this); + } } } -} #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ExpiredHandlerTrackingEntry.cs b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ExpiredHandlerTrackingEntry.cs index 52cea00..eaf8569 100644 --- a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ExpiredHandlerTrackingEntry.cs +++ b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ExpiredHandlerTrackingEntry.cs @@ -10,31 +10,30 @@ using System; using System.Net.Http; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Thread-safety: This class is immutable +/// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/ExpiredHandlerTrackingEntry.cs +/// +internal sealed class ExpiredHandlerTrackingEntry { - /// - /// Thread-safety: This class is immutable - /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/ExpiredHandlerTrackingEntry.cs - /// - internal class ExpiredHandlerTrackingEntry - { - private readonly WeakReference _livenessTracker; + private readonly WeakReference _livenessTracker; - // IMPORTANT: don't cache a reference to `other` or `other.Handler` here. - // We need to allow it to be GC'ed. - public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other) - { - Key = other.Key; + // IMPORTANT: don't cache a reference to `other` or `other.Handler` here. + // We need to allow it to be GC'ed. + public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other) + { + Key = other.Key; - _livenessTracker = new WeakReference(other.Handler); - InnerHandler = other.Handler.InnerHandler; - } + _livenessTracker = new WeakReference(other.Handler); + InnerHandler = other.Handler.InnerHandler; + } - public bool CanDispose => !_livenessTracker.IsAlive; + public bool CanDispose => !_livenessTracker.IsAlive; - public HttpMessageHandler InnerHandler { get; } + public HttpMessageHandler InnerHandler { get; } - public int Key { get; } - } + public int Key { get; } } #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs index 85a0f6e..cd3572e 100644 --- a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs +++ b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/LifetimeTrackingHttpMessageHandler.cs @@ -9,23 +9,22 @@ #if DOTNETCORE using System.Net.Http; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// This a marker used to check if the underlying handler should be disposed. HttpClients +/// share a reference to an instance of this class, and when it goes out of scope the inner handler +/// is eligible to be disposed. +/// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/LifetimeTrackingHttpMessageHandler.cs +/// +internal sealed class LifetimeTrackingHttpMessageHandler : DelegatingHandler { - /// - /// This a marker used to check if the underlying handler should be disposed. HttpClients - /// share a reference to an instance of this class, and when it goes out of scope the inner handler - /// is eligible to be disposed. - /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/LifetimeTrackingHttpMessageHandler.cs - /// - internal class LifetimeTrackingHttpMessageHandler : DelegatingHandler - { - public LifetimeTrackingHttpMessageHandler(HttpMessageHandler innerHandler) - : base(innerHandler) { } + public LifetimeTrackingHttpMessageHandler(HttpMessageHandler innerHandler) + : base(innerHandler) { } - protected override void Dispose(bool disposing) - { - // The lifetime of this is tracked separately by ActiveHandlerTrackingEntry - } + protected override void Dispose(bool disposing) + { + // The lifetime of this is tracked separately by ActiveHandlerTrackingEntry } } #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/RequestDataHttpClientFactory.cs b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/RequestDataHttpClientFactory.cs index f10dc38..33e6da6 100644 --- a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/RequestDataHttpClientFactory.cs +++ b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/RequestDataHttpClientFactory.cs @@ -13,254 +13,253 @@ using System.Net.Http; using System.Threading; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Heavily modified version of DefaultHttpClientFactory, re-purposed for RequestData +/// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs +/// +internal sealed class RequestDataHttpClientFactory : IDisposable { + private readonly Func _createHttpClientHandler; + private static readonly TimerCallback CleanupCallback = (s) => ((RequestDataHttpClientFactory)s).CleanupTimer_Tick(); + private readonly Func> _entryFactory; + + // Default time of 10s for cleanup seems reasonable. + // Quick math: + // 10 distinct named clients * expiry time >= 1s = approximate cleanup queue of 100 items + // + // This seems frequent enough. We also rely on GC occurring to actually trigger disposal. + private readonly TimeSpan _defaultCleanupInterval = TimeSpan.FromSeconds(10); + + // We use a new timer for each regular cleanup cycle, protected with a lock. Note that this scheme + // doesn't give us anything to dispose, as the timer is started/stopped as needed. + // + // There's no need for the factory itself to be disposable. If you stop using it, eventually everything will + // get reclaimed. + private Timer _cleanupTimer; + private readonly object _cleanupTimerLock; + private readonly object _cleanupActiveLock; + + // Collection of 'active' handlers. + // + // Using lazy for synchronization to ensure that only one instance of HttpMessageHandler is created + // for each name. + // + private readonly ConcurrentDictionary> _activeHandlers; + /// - /// Heavily modified version of DefaultHttpClientFactory, re-purposed for RequestData - /// https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs + /// The total number if in use message handlers, this number should not exceed the double digits + /// for any default setup /// - internal class RequestDataHttpClientFactory : IDisposable - { - private readonly Func _createHttpClientHandler; - private static readonly TimerCallback CleanupCallback = (s) => ((RequestDataHttpClientFactory)s).CleanupTimer_Tick(); - private readonly Func> _entryFactory; + public int InUseHandlers => _activeHandlers.Count; - // Default time of 10s for cleanup seems reasonable. - // Quick math: - // 10 distinct named clients * expiry time >= 1s = approximate cleanup queue of 100 items - // - // This seems frequent enough. We also rely on GC occurring to actually trigger disposal. - private readonly TimeSpan _defaultCleanupInterval = TimeSpan.FromSeconds(10); - - // We use a new timer for each regular cleanup cycle, protected with a lock. Note that this scheme - // doesn't give us anything to dispose, as the timer is started/stopped as needed. - // - // There's no need for the factory itself to be disposable. If you stop using it, eventually everything will - // get reclaimed. - private Timer _cleanupTimer; - private readonly object _cleanupTimerLock; - private readonly object _cleanupActiveLock; + private int _removedHandlers; + /// + /// The total amount of handlers that have been swapped over during the lifetime of the application. + /// This can be high depending on how aggressive the DNS caching is setup. + /// + public int RemovedHandlers => _removedHandlers; - // Collection of 'active' handlers. - // - // Using lazy for synchronization to ensure that only one instance of HttpMessageHandler is created - // for each name. - // - private readonly ConcurrentDictionary> _activeHandlers; - - /// - /// The total number if in use message handlers, this number should not exceed the double digits - /// for any default setup - /// - public int InUseHandlers => _activeHandlers.Count; - - private int _removedHandlers; - /// - /// The total amount of handlers that have been swapped over during the lifetime of the application. - /// This can be high depending on how aggressive the DNS caching is setup. - /// - public int RemovedHandlers => _removedHandlers; - - // Collection of 'expired' but not yet disposed handlers. - // - // Used when we're rotating handlers so that we can dispose HttpMessageHandler instances once they - // are eligible for garbage collection. - // - private readonly ConcurrentQueue _expiredHandlers; - private readonly TimerCallback _expiryCallback; + // Collection of 'expired' but not yet disposed handlers. + // + // Used when we're rotating handlers so that we can dispose HttpMessageHandler instances once they + // are eligible for garbage collection. + // + private readonly ConcurrentQueue _expiredHandlers; + private readonly TimerCallback _expiryCallback; - public RequestDataHttpClientFactory(Func createHttpClientHandler) + public RequestDataHttpClientFactory(Func createHttpClientHandler) + { + _createHttpClientHandler = createHttpClientHandler; + // case-sensitive because named options is. + _activeHandlers = new ConcurrentDictionary>(); + _entryFactory = (key, requestData) => { - _createHttpClientHandler = createHttpClientHandler; - // case-sensitive because named options is. - _activeHandlers = new ConcurrentDictionary>(); - _entryFactory = (key, requestData) => - { - return new Lazy(() => CreateHandlerEntry(key, requestData), - LazyThreadSafetyMode.ExecutionAndPublication); - }; + return new Lazy(() => CreateHandlerEntry(key, requestData), + LazyThreadSafetyMode.ExecutionAndPublication); + }; - _expiredHandlers = new ConcurrentQueue(); - _expiryCallback = ExpiryTimer_Tick; + _expiredHandlers = new ConcurrentQueue(); + _expiryCallback = ExpiryTimer_Tick; - _cleanupTimerLock = new object(); - _cleanupActiveLock = new object(); - } - - public HttpClient CreateClient(RequestData requestData) - { - if (requestData == null) throw new ArgumentNullException(nameof(requestData)); + _cleanupTimerLock = new object(); + _cleanupActiveLock = new object(); + } - var key = HttpTransportClient.GetClientKey(requestData); - var handler = CreateHandler(key, requestData); - var client = new HttpClient(handler, disposeHandler: false) - { - Timeout = requestData.RequestTimeout - }; - return client; - } + public HttpClient CreateClient(RequestData requestData) + { + if (requestData == null) throw new ArgumentNullException(nameof(requestData)); - private HttpMessageHandler CreateHandler(int key, RequestData requestData) + var key = HttpTransportClient.GetClientKey(requestData); + var handler = CreateHandler(key, requestData); + var client = new HttpClient(handler, disposeHandler: false) { - if (requestData == null) throw new ArgumentNullException(nameof(requestData)); + Timeout = requestData.RequestTimeout + }; + return client; + } + + private HttpMessageHandler CreateHandler(int key, RequestData requestData) + { + if (requestData == null) throw new ArgumentNullException(nameof(requestData)); #if !NETSTANDARD2_0 && !NETFRAMEWORK - var entry = _activeHandlers.GetOrAdd(key, (k, r) => _entryFactory(k, r), requestData).Value; + var entry = _activeHandlers.GetOrAdd(key, (k, r) => _entryFactory(k, r), requestData).Value; #else - var entry = _activeHandlers.GetOrAdd(key, (k) => _entryFactory(k, requestData)).Value; + var entry = _activeHandlers.GetOrAdd(key, (k) => _entryFactory(k, requestData)).Value; #endif - StartHandlerEntryTimer(entry); + StartHandlerEntryTimer(entry); - return entry.Handler; - } + return entry.Handler; + } - private ActiveHandlerTrackingEntry CreateHandlerEntry(int key, RequestData requestData) - { - // Wrap the handler so we can ensure the inner handler outlives the outer handler. - var handler = new LifetimeTrackingHttpMessageHandler(_createHttpClientHandler(requestData)); + private ActiveHandlerTrackingEntry CreateHandlerEntry(int key, RequestData requestData) + { + // Wrap the handler so we can ensure the inner handler outlives the outer handler. + var handler = new LifetimeTrackingHttpMessageHandler(_createHttpClientHandler(requestData)); - // Note that we can't start the timer here. That would introduce a very very subtle race condition - // with very short expiry times. We need to wait until we've actually handed out the handler once - // to start the timer. - // - // Otherwise it would be possible that we start the timer here, immediately expire it (very short - // timer) and then dispose it without ever creating a client. That would be bad. It's unlikely - // this would happen, but we want to be sure. - return new ActiveHandlerTrackingEntry(key, handler, requestData.DnsRefreshTimeout); - } + // Note that we can't start the timer here. That would introduce a very very subtle race condition + // with very short expiry times. We need to wait until we've actually handed out the handler once + // to start the timer. + // + // Otherwise it would be possible that we start the timer here, immediately expire it (very short + // timer) and then dispose it without ever creating a client. That would be bad. It's unlikely + // this would happen, but we want to be sure. + return new ActiveHandlerTrackingEntry(key, handler, requestData.DnsRefreshTimeout); + } - private void ExpiryTimer_Tick(object state) - { - var active = (ActiveHandlerTrackingEntry)state; - - // The timer callback should be the only one removing from the active collection. If we can't find - // our entry in the collection, then this is a bug. - var removed = _activeHandlers.TryRemove(active.Key, out var found); - if (removed) - Interlocked.Increment(ref _removedHandlers); - Debug.Assert(removed, "Entry not found. We should always be able to remove the entry"); - // ReSharper disable once RedundantNameQualifier - Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced"); - - // At this point the handler is no longer 'active' and will not be handed out to any new clients. - // However we haven't dropped our strong reference to the handler, so we can't yet determine if - // there are still any other outstanding references (we know there is at least one). - // - // We use a different state object to track expired handlers. This allows any other thread that acquired - // the 'active' entry to use it without safety problems. - var expired = new ExpiredHandlerTrackingEntry(active); - _expiredHandlers.Enqueue(expired); + private void ExpiryTimer_Tick(object state) + { + var active = (ActiveHandlerTrackingEntry)state; + + // The timer callback should be the only one removing from the active collection. If we can't find + // our entry in the collection, then this is a bug. + var removed = _activeHandlers.TryRemove(active.Key, out var found); + if (removed) + Interlocked.Increment(ref _removedHandlers); + Debug.Assert(removed, "Entry not found. We should always be able to remove the entry"); + // ReSharper disable once RedundantNameQualifier + Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced"); + + // At this point the handler is no longer 'active' and will not be handed out to any new clients. + // However we haven't dropped our strong reference to the handler, so we can't yet determine if + // there are still any other outstanding references (we know there is at least one). + // + // We use a different state object to track expired handlers. This allows any other thread that acquired + // the 'active' entry to use it without safety problems. + var expired = new ExpiredHandlerTrackingEntry(active); + _expiredHandlers.Enqueue(expired); - StartCleanupTimer(); - } + StartCleanupTimer(); + } - protected virtual void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry) => entry.StartExpiryTimer(_expiryCallback); + private void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry) => entry.StartExpiryTimer(_expiryCallback); - protected virtual void StartCleanupTimer() - { - lock (_cleanupTimerLock) - _cleanupTimer ??= NonCapturingTimer.Create(CleanupCallback, this, _defaultCleanupInterval, Timeout.InfiniteTimeSpan); - } + private void StartCleanupTimer() + { + lock (_cleanupTimerLock) + _cleanupTimer ??= NonCapturingTimer.Create(CleanupCallback, this, _defaultCleanupInterval, Timeout.InfiniteTimeSpan); + } - protected virtual void StopCleanupTimer() + private void StopCleanupTimer() + { + lock (_cleanupTimerLock) { - lock (_cleanupTimerLock) - { - _cleanupTimer?.Dispose(); - _cleanupTimer = null; - } + _cleanupTimer?.Dispose(); + _cleanupTimer = null; } + } - private void CleanupTimer_Tick() + private void CleanupTimer_Tick() + { + // Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup. + // + // With the scheme we're using it's possible we could end up with some redundant cleanup operations. + // This is expected and fine. + // + // An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it + // would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out + // whether we need to start the timer. + StopCleanupTimer(); + + if (!Monitor.TryEnter(_cleanupActiveLock)) { - // Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup. + // We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes + // a long time for some reason. Since we're running user code inside Dispose, it's definitely + // possible. // - // With the scheme we're using it's possible we could end up with some redundant cleanup operations. - // This is expected and fine. - // - // An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it - // would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out - // whether we need to start the timer. - StopCleanupTimer(); + // If we end up in that position, just make sure the timer gets started again. It should be cheap + // to run a 'no-op' cleanup. + StartCleanupTimer(); + return; + } - if (!Monitor.TryEnter(_cleanupActiveLock)) - { - // We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes - // a long time for some reason. Since we're running user code inside Dispose, it's definitely - // possible. - // - // If we end up in that position, just make sure the timer gets started again. It should be cheap - // to run a 'no-op' cleanup. - StartCleanupTimer(); - return; - } + try + { + var initialCount = _expiredHandlers.Count; - try + for (var i = 0; i < initialCount; i++) { - var initialCount = _expiredHandlers.Count; + // Since we're the only one removing from _expired, TryDequeue must always succeed. + _expiredHandlers.TryDequeue(out var entry); + Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue"); - for (var i = 0; i < initialCount; i++) + if (entry.CanDispose) { - // Since we're the only one removing from _expired, TryDequeue must always succeed. - _expiredHandlers.TryDequeue(out var entry); - Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue"); - - if (entry.CanDispose) + try { - try - { - entry.InnerHandler.Dispose(); - } - catch (Exception) - { - // ignored (ignored in HttpClientFactory too) - } + entry.InnerHandler.Dispose(); + } + catch (Exception) + { + // ignored (ignored in HttpClientFactory too) } - // If the entry is still live, put it back in the queue so we can process it - // during the next cleanup cycle. - else - _expiredHandlers.Enqueue(entry); } + // If the entry is still live, put it back in the queue so we can process it + // during the next cleanup cycle. + else + _expiredHandlers.Enqueue(entry); } - finally - { - Monitor.Exit(_cleanupActiveLock); - } - - // We didn't totally empty the cleanup queue, try again later. - if (_expiredHandlers.Count > 0) StartCleanupTimer(); } - - public void Dispose() + finally { - //try to cleanup nicely - CleanupTimer_Tick(); - _cleanupTimer?.Dispose(); + Monitor.Exit(_cleanupActiveLock); + } - //CleanupTimer might not cleanup everything because it will only dispose if the WeakReference allows it. - // here we forcefully dispose a Client -> ConnectionSettings -> Connection -> RequestDataHttpClientFactory - var attempts = 0; - do + // We didn't totally empty the cleanup queue, try again later. + if (_expiredHandlers.Count > 0) StartCleanupTimer(); + } + + public void Dispose() + { + //try to cleanup nicely + CleanupTimer_Tick(); + _cleanupTimer?.Dispose(); + + //CleanupTimer might not cleanup everything because it will only dispose if the WeakReference allows it. + // here we forcefully dispose a Client -> ConnectionSettings -> Connection -> RequestDataHttpClientFactory + var attempts = 0; + do + { + attempts++; + var initialCount = _expiredHandlers.Count; + for (var i = 0; i < initialCount; i++) { - attempts++; - var initialCount = _expiredHandlers.Count; - for (var i = 0; i < initialCount; i++) + // Since we're the only one removing from _expired, TryDequeue must always succeed. + _expiredHandlers.TryDequeue(out var entry); + try { - // Since we're the only one removing from _expired, TryDequeue must always succeed. - _expiredHandlers.TryDequeue(out var entry); - try - { - entry?.InnerHandler.Dispose(); - } - catch (Exception) - { - // ignored (ignored in HttpClientFactory too) - } + entry?.InnerHandler.Dispose(); + } + catch (Exception) + { + // ignored (ignored in HttpClientFactory too) } - } while (attempts < 5 && _expiredHandlers.Count > 0); + } + } while (attempts < 5 && _expiredHandlers.Count > 0); - } } } #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ValueStopWatch.cs b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ValueStopWatch.cs index ef161b9..d4717de 100644 --- a/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ValueStopWatch.cs +++ b/src/Elastic.Transport/Components/TransportClient/HandlerTracking/ValueStopWatch.cs @@ -10,36 +10,35 @@ using System; using System.Threading; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A convenience API for interacting with System.Threading.Timer in a way +/// that doesn't capture the ExecutionContext. We should be using this (or equivalent) +/// everywhere we use timers to avoid rooting any values stored in asynclocals. +/// https://github.com/dotnet/runtime/blob/master/src/libraries/Common/src/Extensions/ValueStopwatch/ValueStopwatch.cs +/// +internal static class NonCapturingTimer { - /// - /// A convenience API for interacting with System.Threading.Timer in a way - /// that doesn't capture the ExecutionContext. We should be using this (or equivalent) - /// everywhere we use timers to avoid rooting any values stored in asynclocals. - /// https://github.com/dotnet/runtime/blob/master/src/libraries/Common/src/Extensions/ValueStopwatch/ValueStopwatch.cs - /// - internal static class NonCapturingTimer + public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { - public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); + if (callback == null) throw new ArgumentNullException(nameof(callback)); - // Don't capture the current ExecutionContext and its AsyncLocals onto the timer - var restoreFlow = false; - try - { - if (ExecutionContext.IsFlowSuppressed()) return new Timer(callback, state, dueTime, period); + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (ExecutionContext.IsFlowSuppressed()) return new Timer(callback, state, dueTime, period); - ExecutionContext.SuppressFlow(); - restoreFlow = true; + ExecutionContext.SuppressFlow(); + restoreFlow = true; - return new Timer(callback, state, dueTime, period); - } - finally - { - // Restore the current ExecutionContext - if (restoreFlow) ExecutionContext.RestoreFlow(); - } + return new Timer(callback, state, dueTime, period); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) ExecutionContext.RestoreFlow(); } } } diff --git a/src/Elastic.Transport/Components/TransportClient/HttpMethod.cs b/src/Elastic.Transport/Components/TransportClient/HttpMethod.cs index 5927d2e..c0fd7e8 100644 --- a/src/Elastic.Transport/Components/TransportClient/HttpMethod.cs +++ b/src/Elastic.Transport/Components/TransportClient/HttpMethod.cs @@ -6,27 +6,26 @@ // ReSharper disable InconsistentNaming -namespace Elastic.Transport +namespace Elastic.Transport; + +/// Http Method of the API call to be performed +public enum HttpMethod { - /// Http Method of the API call to be performed - public enum HttpMethod - { - [EnumMember(Value = "GET")] + [EnumMember(Value = "GET")] // These really do not need xmldocs, leave it to the reader if they feel inspired :) #pragma warning disable 1591 - GET, + GET, - [EnumMember(Value = "POST")] - POST, + [EnumMember(Value = "POST")] + POST, - [EnumMember(Value = "PUT")] - PUT, + [EnumMember(Value = "PUT")] + PUT, - [EnumMember(Value = "DELETE")] - DELETE, + [EnumMember(Value = "DELETE")] + DELETE, - [EnumMember(Value = "HEAD")] - HEAD + [EnumMember(Value = "HEAD")] + HEAD #pragma warning restore 1591 - } } diff --git a/src/Elastic.Transport/Components/TransportClient/HttpTransportClient-FullFramework.cs b/src/Elastic.Transport/Components/TransportClient/HttpTransportClient-FullFramework.cs index d204f30..5fda84c 100644 --- a/src/Elastic.Transport/Components/TransportClient/HttpTransportClient-FullFramework.cs +++ b/src/Elastic.Transport/Components/TransportClient/HttpTransportClient-FullFramework.cs @@ -7,7 +7,7 @@ namespace Elastic.Transport { - /// The default ITransportClient implementation. Uses on the current .NET desktop framework. - public class HttpTransportClient : HttpWebRequestTransportClient { } + /// The default TransportClient implementation. Uses on the current .NET desktop framework. + public sealed class HttpTransportClient : HttpWebRequestTransportClient { } } #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HttpTransportClient.cs b/src/Elastic.Transport/Components/TransportClient/HttpTransportClient.cs index 43265d7..29e33b8 100644 --- a/src/Elastic.Transport/Components/TransportClient/HttpTransportClient.cs +++ b/src/Elastic.Transport/Components/TransportClient/HttpTransportClient.cs @@ -14,506 +14,487 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.NetworkInformation; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Elastic.Transport.Diagnostics; using Elastic.Transport.Extensions; using static System.Net.DecompressionMethods; -namespace Elastic.Transport -{ - internal class WebProxy : IWebProxy - { - private readonly Uri _uri; - - public WebProxy(Uri uri) => _uri = uri; - - public ICredentials Credentials { get; set; } - - public Uri GetProxy(Uri destination) => _uri; - - public bool IsBypassed(Uri host) => host.IsLoopback; - } +namespace Elastic.Transport; - /// The default ITransportClient implementation. Uses . - public class HttpTransportClient : ITransportClient - { - private static readonly string MissingConnectionLimitMethodError = - $"Your target platform does not support {nameof(TransportConfiguration.ConnectionLimit)}" - + $" please set {nameof(TransportConfiguration.ConnectionLimit)} to -1 on your connection configuration/settings." - + $" this will cause the {nameof(HttpClientHandler.MaxConnectionsPerServer)} not to be set on {nameof(HttpClientHandler)}"; +/// The default TransportClient implementation. Uses . +public class HttpTransportClient : TransportClient +{ + private static readonly string MissingConnectionLimitMethodError = + $"Your target platform does not support {nameof(TransportConfiguration.ConnectionLimit)}" + + $" please set {nameof(TransportConfiguration.ConnectionLimit)} to -1 on your connection configuration/settings." + + $" this will cause the {nameof(HttpClientHandler.MaxConnectionsPerServer)} not to be set on {nameof(HttpClientHandler)}"; - private string _expectedCertificateFingerprint; + private string _expectedCertificateFingerprint; - /// - public HttpTransportClient() => HttpClientFactory = new RequestDataHttpClientFactory(r => CreateHttpClientHandler(r)); + /// + public HttpTransportClient() => HttpClientFactory = new RequestDataHttpClientFactory(r => CreateHttpClientHandler(r)); - /// - public int InUseHandlers => HttpClientFactory.InUseHandlers; + /// + public int InUseHandlers => HttpClientFactory.InUseHandlers; - /// - public int RemovedHandlers => HttpClientFactory.RemovedHandlers; + /// + public int RemovedHandlers => HttpClientFactory.RemovedHandlers; - private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.HttpConnection.SourceName); + private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.HttpConnection.SourceName); - private RequestDataHttpClientFactory HttpClientFactory { get; } + private RequestDataHttpClientFactory HttpClientFactory { get; } - /// - public virtual TResponse Request(RequestData requestData) - where TResponse : class, ITransportResponse, new() + /// + public override TResponse Request(RequestData requestData) + { + var client = GetClient(requestData); + HttpResponseMessage responseMessage; + int? statusCode = null; + Stream responseStream = null; + Exception ex = null; + string mimeType = null; + long contentLength = -1; + IDisposable receive = DiagnosticSources.SingletonDisposable; + ReadOnlyDictionary tcpStats = null; + ReadOnlyDictionary threadPoolStats = null; + Dictionary> responseHeaders = null; + + try { - var client = GetClient(requestData); - HttpResponseMessage responseMessage; - int? statusCode = null; - Stream responseStream = null; - Exception ex = null; - string mimeType = null; - long contentLength = -1; - IDisposable receive = DiagnosticSources.SingletonDisposable; - ReadOnlyDictionary tcpStats = null; - ReadOnlyDictionary threadPoolStats = null; - Dictionary> responseHeaders = null; + var requestMessage = CreateHttpRequestMessage(requestData); - try - { - var requestMessage = CreateHttpRequestMessage(requestData); + if (requestData.PostData != null) + SetContent(requestMessage, requestData); - if (requestData.PostData != null) - SetContent(requestMessage, requestData); - - using (requestMessage?.Content ?? (IDisposable)Stream.Null) - using (var d = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.SendAndReceiveHeaders, requestData)) - { - if (requestData.TcpStats) - tcpStats = TcpStats.GetStates(); + using (requestMessage?.Content ?? (IDisposable)Stream.Null) + using (var d = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.SendAndReceiveHeaders, requestData)) + { + if (requestData.TcpStats) + tcpStats = TcpStats.GetStates(); - if (requestData.ThreadPoolStats) - threadPoolStats = ThreadPoolStats.GetStats(); + if (requestData.ThreadPoolStats) + threadPoolStats = ThreadPoolStats.GetStats(); #if NET5_0_OR_GREATER - responseMessage = client.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead); + responseMessage = client.Send(requestMessage, HttpCompletionOption.ResponseHeadersRead); #else - responseMessage = client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult(); + responseMessage = client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult(); #endif - statusCode = (int)responseMessage.StatusCode; - d.EndState = statusCode; - } + statusCode = (int)responseMessage.StatusCode; + d.EndState = statusCode; + } - requestData.MadeItToResponse = true; - responseHeaders = ParseHeaders(requestData, responseMessage, responseHeaders); - contentLength = responseMessage.Content.Headers.ContentLength ?? -1; - mimeType = responseMessage.Content.Headers.ContentType?.MediaType; + requestData.MadeItToResponse = true; + responseHeaders = ParseHeaders(requestData, responseMessage, responseHeaders); + contentLength = responseMessage.Content.Headers.ContentLength ?? -1; + mimeType = responseMessage.Content.Headers.ContentType?.MediaType; - if (responseMessage.Content != null) - { - receive = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.ReceiveBody, requestData, statusCode); + if (responseMessage.Content != null) + { + receive = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.ReceiveBody, requestData, statusCode); #if NET5_0_OR_GREATER - responseStream = responseMessage.Content.ReadAsStream(); + responseStream = responseMessage.Content.ReadAsStream(); #else - responseStream = responseMessage.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + responseStream = responseMessage.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); #endif - } } - catch (TaskCanceledException e) - { - ex = e; - } - catch (HttpRequestException e) - { - ex = e; - } - using (receive) - using (responseStream ??= Stream.Null) - { - var response = requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, ex, statusCode, responseHeaders, responseStream, mimeType, - contentLength, threadPoolStats, tcpStats); + } + catch (TaskCanceledException e) + { + ex = e; + } + catch (HttpRequestException e) + { + ex = e; + } + using (receive) + using (responseStream ??= Stream.Null) + { + var response = requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, ex, statusCode, responseHeaders, responseStream, mimeType, + contentLength, threadPoolStats, tcpStats); - return response; - } + return response; } + } - /// - public virtual async Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) - where TResponse : class, ITransportResponse, new() + /// + public override async Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) + { + var client = GetClient(requestData); + HttpResponseMessage responseMessage; + int? statusCode = null; + Stream responseStream = null; + Exception ex = null; + string mimeType = null; + long contentLength = -1; + IDisposable receive = DiagnosticSources.SingletonDisposable; + ReadOnlyDictionary tcpStats = null; + ReadOnlyDictionary threadPoolStats = null; + Dictionary> responseHeaders = null; + requestData.IsAsync = true; + + try { - var client = GetClient(requestData); - HttpResponseMessage responseMessage; - int? statusCode = null; - Stream responseStream = null; - Exception ex = null; - string mimeType = null; - long contentLength = -1; - IDisposable receive = DiagnosticSources.SingletonDisposable; - ReadOnlyDictionary tcpStats = null; - ReadOnlyDictionary threadPoolStats = null; - Dictionary> responseHeaders = null; - requestData.IsAsync = true; + var requestMessage = CreateHttpRequestMessage(requestData); - try - { - var requestMessage = CreateHttpRequestMessage(requestData); - - if (requestData.PostData != null) - await SetContentAsync(requestMessage, requestData, cancellationToken).ConfigureAwait(false); + if (requestData.PostData != null) + await SetContentAsync(requestMessage, requestData, cancellationToken).ConfigureAwait(false); - using (requestMessage?.Content ?? (IDisposable)Stream.Null) - using (var d = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.SendAndReceiveHeaders, requestData)) - { - if (requestData.TcpStats) - tcpStats = TcpStats.GetStates(); + using (requestMessage?.Content ?? (IDisposable)Stream.Null) + using (var d = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.SendAndReceiveHeaders, requestData)) + { + if (requestData.TcpStats) + tcpStats = TcpStats.GetStates(); - if (requestData.ThreadPoolStats) - threadPoolStats = ThreadPoolStats.GetStats(); + if (requestData.ThreadPoolStats) + threadPoolStats = ThreadPoolStats.GetStats(); - responseMessage = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken) - .ConfigureAwait(false); - statusCode = (int)responseMessage.StatusCode; - d.EndState = statusCode; - } + responseMessage = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + statusCode = (int)responseMessage.StatusCode; + d.EndState = statusCode; + } - requestData.MadeItToResponse = true; - mimeType = responseMessage.Content.Headers.ContentType?.MediaType; - contentLength = responseMessage.Content.Headers.ContentLength ?? -1; - responseHeaders = ParseHeaders(requestData, responseMessage, responseHeaders); + requestData.MadeItToResponse = true; + mimeType = responseMessage.Content.Headers.ContentType?.MediaType; + contentLength = responseMessage.Content.Headers.ContentLength ?? -1; + responseHeaders = ParseHeaders(requestData, responseMessage, responseHeaders); - if (responseMessage.Content != null) - { - receive = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.ReceiveBody, requestData, statusCode); - responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); - } - } - catch (TaskCanceledException e) + if (responseMessage.Content != null) { - ex = e; - } - catch (HttpRequestException e) - { - ex = e; - } - using (receive) - using (responseStream ??= Stream.Null) - { - var response = await requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponseAsync - (requestData, ex, statusCode, responseHeaders, responseStream, mimeType, contentLength, threadPoolStats, tcpStats, cancellationToken) - .ConfigureAwait(false); - return response; + receive = DiagnosticSource.Diagnose(DiagnosticSources.HttpConnection.ReceiveBody, requestData, statusCode); + responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); } } + catch (TaskCanceledException e) + { + ex = e; + } + catch (HttpRequestException e) + { + ex = e; + } + using (receive) + using (responseStream ??= Stream.Null) + { + var response = await requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponseAsync + (requestData, ex, statusCode, responseHeaders, responseStream, mimeType, contentLength, threadPoolStats, tcpStats, cancellationToken) + .ConfigureAwait(false); + return response; + } + } - private static Dictionary> ParseHeaders(RequestData requestData, HttpResponseMessage responseMessage, Dictionary> responseHeaders) + private static Dictionary> ParseHeaders(RequestData requestData, HttpResponseMessage responseMessage, Dictionary> responseHeaders) + { + if (requestData.ParseAllHeaders) { - if (requestData.ParseAllHeaders) + foreach (var header in responseMessage.Headers) { - foreach (var header in responseMessage.Headers) - { - responseHeaders ??= new Dictionary>(); - responseHeaders.Add(header.Key, header.Value); - } + responseHeaders ??= new Dictionary>(); + responseHeaders.Add(header.Key, header.Value); } - else if (requestData.ResponseHeadersToParse.Count > 0) + } + else if (requestData.ResponseHeadersToParse.Count > 0) + { + foreach (var headerToParse in requestData.ResponseHeadersToParse) { - foreach (var headerToParse in requestData.ResponseHeadersToParse) + if (responseMessage.Headers.TryGetValues(headerToParse, out var values)) { - if (responseMessage.Headers.TryGetValues(headerToParse, out var values)) - { - responseHeaders ??= new Dictionary>(); - responseHeaders.Add(headerToParse, values); - } + responseHeaders ??= new Dictionary>(); + responseHeaders.Add(headerToParse, values); } } - - return responseHeaders; } - void IDisposable.Dispose() => DisposeManagedResources(); - - private HttpClient GetClient(RequestData requestData) => HttpClientFactory.CreateClient(requestData); - - /// - /// Creates an instance of using the . - /// This method is virtual so subclasses of can modify the instance if needed. - /// - /// An instance of describing where and how to call out to - /// - /// Can throw if is set but the platform does - /// not allow this to be set on - /// - protected virtual HttpMessageHandler CreateHttpClientHandler(RequestData requestData) - { - var handler = new HttpClientHandler { AutomaticDecompression = requestData.HttpCompression ? GZip | Deflate : None, }; + return responseHeaders; + } - // same limit as desktop clr - if (requestData.ConnectionSettings.ConnectionLimit > 0) - try - { - handler.MaxConnectionsPerServer = requestData.ConnectionSettings.ConnectionLimit; - } - catch (MissingMethodException e) - { - throw new Exception(MissingConnectionLimitMethodError, e); - } - catch (PlatformNotSupportedException e) - { - throw new Exception(MissingConnectionLimitMethodError, e); - } + private HttpClient GetClient(RequestData requestData) => HttpClientFactory.CreateClient(requestData); + + /// + /// Creates an instance of using the . + /// This method is virtual so subclasses of can modify the instance if needed. + /// + /// An instance of describing where and how to call out to + /// + /// Can throw if is set but the platform does + /// not allow this to be set on + /// + protected virtual HttpMessageHandler CreateHttpClientHandler(RequestData requestData) + { + var handler = new HttpClientHandler { AutomaticDecompression = requestData.HttpCompression ? GZip | Deflate : None, }; - if (!requestData.ProxyAddress.IsNullOrEmpty()) + // same limit as desktop clr + if (requestData.ConnectionSettings.ConnectionLimit > 0) + try { - var uri = new Uri(requestData.ProxyAddress); - var proxy = new WebProxy(uri); - if (!string.IsNullOrEmpty(requestData.ProxyUsername)) - { - var credentials = new NetworkCredential(requestData.ProxyUsername, requestData.ProxyPassword); - proxy.Credentials = credentials; - } - handler.Proxy = proxy; + handler.MaxConnectionsPerServer = requestData.ConnectionSettings.ConnectionLimit; } - else if (requestData.DisableAutomaticProxyDetection) handler.UseProxy = false; - - // Configure certificate validation - var callback = requestData.ConnectionSettings?.ServerCertificateValidationCallback; - if (callback != null && handler.ServerCertificateCustomValidationCallback == null) + catch (MissingMethodException e) { - handler.ServerCertificateCustomValidationCallback = callback; + throw new Exception(MissingConnectionLimitMethodError, e); } - else if (!string.IsNullOrEmpty(requestData.ConnectionSettings.CertificateFingerprint)) + catch (PlatformNotSupportedException e) { - handler.ServerCertificateCustomValidationCallback = (request, certificate, chain, policyErrors) => - { - if (certificate is null && chain is null) return false; - - // The "cleaned", expected fingerprint is cached to avoid repeated cost of converting it to a comparable form. - _expectedCertificateFingerprint ??= CertificateHelpers.ComparableFingerprint(requestData.ConnectionSettings.CertificateFingerprint); - - // If there is a chain, check each certificate up to the root - if (chain is not null) - { - foreach (var element in chain.ChainElements) - { - if (CertificateHelpers.ValidateCertificateFingerprint(element.Certificate, _expectedCertificateFingerprint)) - return true; - } - } - - // Otherwise, check the certificate - return CertificateHelpers.ValidateCertificateFingerprint(certificate, _expectedCertificateFingerprint); - }; + throw new Exception(MissingConnectionLimitMethodError, e); } - if (requestData.ClientCertificates != null) + if (!requestData.ProxyAddress.IsNullOrEmpty()) + { + var uri = new Uri(requestData.ProxyAddress); + var proxy = new WebProxy(uri); + if (!string.IsNullOrEmpty(requestData.ProxyUsername)) { - handler.ClientCertificateOptions = ClientCertificateOption.Manual; - handler.ClientCertificates.AddRange(requestData.ClientCertificates); + var credentials = new NetworkCredential(requestData.ProxyUsername, requestData.ProxyPassword); + proxy.Credentials = credentials; } - - return handler; + handler.Proxy = proxy; } + else if (requestData.DisableAutomaticProxyDetection) handler.UseProxy = false; - private string ComparableFingerprint(string fingerprint) + // Configure certificate validation + var callback = requestData.ConnectionSettings?.ServerCertificateValidationCallback; + if (callback != null && handler.ServerCertificateCustomValidationCallback == null) + { + handler.ServerCertificateCustomValidationCallback = callback; + } + else if (!string.IsNullOrEmpty(requestData.ConnectionSettings.CertificateFingerprint)) { - var finalFingerprint = fingerprint; + handler.ServerCertificateCustomValidationCallback = (request, certificate, chain, policyErrors) => + { + if (certificate is null && chain is null) return false; - if (fingerprint.Contains(':')) - finalFingerprint = fingerprint.Replace(":", string.Empty); + // The "cleaned", expected fingerprint is cached to avoid repeated cost of converting it to a comparable form. + _expectedCertificateFingerprint ??= CertificateHelpers.ComparableFingerprint(requestData.ConnectionSettings.CertificateFingerprint); - return finalFingerprint; + // If there is a chain, check each certificate up to the root + if (chain is not null) + { + foreach (var element in chain.ChainElements) + { + if (CertificateHelpers.ValidateCertificateFingerprint(element.Certificate, _expectedCertificateFingerprint)) + return true; + } + } + + // Otherwise, check the certificate + return CertificateHelpers.ValidateCertificateFingerprint(certificate, _expectedCertificateFingerprint); + }; } - /// - /// Creates an instance of using the . - /// This method is virtual so subclasses of can modify the instance if needed. - /// - /// An instance of describing where and how to call out to - /// - /// Can throw if is set but the platform does - /// not allow this to be set on - /// - protected virtual HttpRequestMessage CreateHttpRequestMessage(RequestData requestData) + if (requestData.ClientCertificates != null) { - var request = CreateRequestMessage(requestData); - SetAuthenticationIfNeeded(request, requestData); - return request; + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + handler.ClientCertificates.AddRange(requestData.ClientCertificates); } - /// Isolated hook for subclasses to set authentication on - /// The instance of that needs authentication details - /// An object describing where and how we want to call out to - protected virtual void SetAuthenticationIfNeeded(HttpRequestMessage requestMessage, RequestData requestData) - { - //If user manually specifies an Authorization Header give it preference - if (requestData.Headers.HasKeys() && requestData.Headers.AllKeys.Contains("Authorization")) - { - var header = AuthenticationHeaderValue.Parse(requestData.Headers["Authorization"]); - requestMessage.Headers.Authorization = header; - return; - } + return handler; + } - SetConfiguredAuthenticationHeaderIfNeeded(requestMessage, requestData); - } + private string ComparableFingerprint(string fingerprint) + { + var finalFingerprint = fingerprint; - private static void SetConfiguredAuthenticationHeaderIfNeeded(HttpRequestMessage requestMessage, RequestData requestData) - { - // Basic auth credentials take the following precedence (highest -> lowest): - // 1 - Specified with the URI (highest precedence) - // 2 - Specified on the request - // 3 - Specified at the global ITransportClientSettings level (lowest precedence) - - string parameters = null; - string scheme = null; - if (!requestData.Uri.UserInfo.IsNullOrEmpty()) - { - parameters = BasicAuthentication.GetBase64String(Uri.UnescapeDataString(requestData.Uri.UserInfo)); - scheme = BasicAuthentication.BasicAuthenticationScheme; - } - else if (requestData.AuthenticationHeader != null && requestData.AuthenticationHeader.TryGetAuthorizationParameters(out var v)) - { - parameters = v; - scheme = requestData.AuthenticationHeader.AuthScheme; - } + if (fingerprint.Contains(':')) + finalFingerprint = fingerprint.Replace(":", string.Empty); + + return finalFingerprint; + } - if (parameters.IsNullOrEmpty()) return; + /// + /// Creates an instance of using the . + /// This method is virtual so subclasses of can modify the instance if needed. + /// + /// An instance of describing where and how to call out to + /// + /// Can throw if is set but the platform does + /// not allow this to be set on + /// + internal HttpRequestMessage CreateHttpRequestMessage(RequestData requestData) + { + var request = CreateRequestMessage(requestData); + SetAuthenticationIfNeeded(request, requestData); + return request; + } - requestMessage.Headers.Authorization = new AuthenticationHeaderValue(scheme, parameters); + /// Isolated hook for subclasses to set authentication on + /// The instance of that needs authentication details + /// An object describing where and how we want to call out to + internal void SetAuthenticationIfNeeded(HttpRequestMessage requestMessage, RequestData requestData) + { + //If user manually specifies an Authorization Header give it preference + if (requestData.Headers.HasKeys() && requestData.Headers.AllKeys.Contains("Authorization")) + { + var header = AuthenticationHeaderValue.Parse(requestData.Headers["Authorization"]); + requestMessage.Headers.Authorization = header; + return; } - private static HttpRequestMessage CreateRequestMessage(RequestData requestData) + SetConfiguredAuthenticationHeaderIfNeeded(requestMessage, requestData); + } + + private static void SetConfiguredAuthenticationHeaderIfNeeded(HttpRequestMessage requestMessage, RequestData requestData) + { + // Basic auth credentials take the following precedence (highest -> lowest): + // 1 - Specified with the URI (highest precedence) + // 2 - Specified on the request + // 3 - Specified at the global TransportClientSettings level (lowest precedence) + + string parameters = null; + string scheme = null; + if (!requestData.Uri.UserInfo.IsNullOrEmpty()) + { + parameters = BasicAuthentication.GetBase64String(Uri.UnescapeDataString(requestData.Uri.UserInfo)); + scheme = BasicAuthentication.BasicAuthenticationScheme; + } + else if (requestData.AuthenticationHeader != null && requestData.AuthenticationHeader.TryGetAuthorizationParameters(out var v)) { - var method = ConvertHttpMethod(requestData.Method); - var requestMessage = new HttpRequestMessage(method, requestData.Uri); + parameters = v; + scheme = requestData.AuthenticationHeader.AuthScheme; + } + + if (parameters.IsNullOrEmpty()) return; - if (requestData.Headers != null) - foreach (string key in requestData.Headers) - requestMessage.Headers.TryAddWithoutValidation(key, requestData.Headers.GetValues(key)); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue(scheme, parameters); + } - requestMessage.Headers.Connection.Clear(); - requestMessage.Headers.ConnectionClose = false; - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(requestData.Accept)); + private static HttpRequestMessage CreateRequestMessage(RequestData requestData) + { + var method = ConvertHttpMethod(requestData.Method); + var requestMessage = new HttpRequestMessage(method, requestData.Uri); - var userAgent = requestData.UserAgent?.ToString(); - if (!string.IsNullOrWhiteSpace(userAgent)) - { - requestMessage.Headers.UserAgent.Clear(); - requestMessage.Headers.UserAgent.TryParseAdd(userAgent); - } + if (requestData.Headers != null) + foreach (string key in requestData.Headers) + requestMessage.Headers.TryAddWithoutValidation(key, requestData.Headers.GetValues(key)); - if (!requestData.RunAs.IsNullOrEmpty()) - requestMessage.Headers.Add(RequestData.RunAsSecurityHeader, requestData.RunAs); + requestMessage.Headers.Connection.Clear(); + requestMessage.Headers.ConnectionClose = false; + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(requestData.Accept)); - if (requestData.MetaHeaderProvider is not null) - { - var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData); + var userAgent = requestData.UserAgent?.ToString(); + if (!string.IsNullOrWhiteSpace(userAgent)) + { + requestMessage.Headers.UserAgent.Clear(); + requestMessage.Headers.UserAgent.TryParseAdd(userAgent); + } - if (!string.IsNullOrEmpty(value)) - requestMessage.Headers.TryAddWithoutValidation(requestData.MetaHeaderProvider.HeaderName, value); - } + if (!requestData.RunAs.IsNullOrEmpty()) + requestMessage.Headers.Add(RequestData.RunAsSecurityHeader, requestData.RunAs); - return requestMessage; + if (requestData.MetaHeaderProvider is not null) + { + var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData); + + if (!string.IsNullOrEmpty(value)) + requestMessage.Headers.TryAddWithoutValidation(requestData.MetaHeaderProvider.HeaderName, value); } - private static void SetContent(HttpRequestMessage message, RequestData requestData) + return requestMessage; + } + + private static void SetContent(HttpRequestMessage message, RequestData requestData) + { + if (requestData.TransferEncodingChunked) + message.Content = new RequestDataContent(requestData); + else { - if (requestData.TransferEncodingChunked) - message.Content = new RequestDataContent(requestData); - else + var stream = requestData.MemoryStreamFactory.Create(); + if (requestData.HttpCompression) { - var stream = requestData.MemoryStreamFactory.Create(); - if (requestData.HttpCompression) - { - using var zipStream = new GZipStream(stream, CompressionMode.Compress, true); - requestData.PostData.Write(zipStream, requestData.ConnectionSettings); - } - else - requestData.PostData.Write(stream, requestData.ConnectionSettings); + using var zipStream = new GZipStream(stream, CompressionMode.Compress, true); + requestData.PostData.Write(zipStream, requestData.ConnectionSettings); + } + else + requestData.PostData.Write(stream, requestData.ConnectionSettings); - // the written bytes are uncompressed, so can only be used when http compression isn't used - if (requestData.PostData.DisableDirectStreaming.GetValueOrDefault(false) && !requestData.HttpCompression) - { - message.Content = new ByteArrayContent(requestData.PostData.WrittenBytes); - stream.Dispose(); - } - else - { - stream.Position = 0; - message.Content = new StreamContent(stream); - } + // the written bytes are uncompressed, so can only be used when http compression isn't used + if (requestData.PostData.DisableDirectStreaming.GetValueOrDefault(false) && !requestData.HttpCompression) + { + message.Content = new ByteArrayContent(requestData.PostData.WrittenBytes); + stream.Dispose(); + } + else + { + stream.Position = 0; + message.Content = new StreamContent(stream); + } - if (requestData.HttpCompression) - message.Content.Headers.ContentEncoding.Add("gzip"); + if (requestData.HttpCompression) + message.Content.Headers.ContentEncoding.Add("gzip"); - message.Content.Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); - } + message.Content.Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); } + } - private static async Task SetContentAsync(HttpRequestMessage message, RequestData requestData, CancellationToken cancellationToken) + private static async Task SetContentAsync(HttpRequestMessage message, RequestData requestData, CancellationToken cancellationToken) + { + if (requestData.TransferEncodingChunked) + message.Content = new RequestDataContent(requestData, cancellationToken); + else { - if (requestData.TransferEncodingChunked) - message.Content = new RequestDataContent(requestData, cancellationToken); - else + var stream = requestData.MemoryStreamFactory.Create(); + if (requestData.HttpCompression) { - var stream = requestData.MemoryStreamFactory.Create(); - if (requestData.HttpCompression) - { - using var zipStream = new GZipStream(stream, CompressionMode.Compress, true); - await requestData.PostData.WriteAsync(zipStream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); - } - else - await requestData.PostData.WriteAsync(stream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); + using var zipStream = new GZipStream(stream, CompressionMode.Compress, true); + await requestData.PostData.WriteAsync(zipStream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); + } + else + await requestData.PostData.WriteAsync(stream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); - // the written bytes are uncompressed, so can only be used when http compression isn't used - if (requestData.PostData.DisableDirectStreaming.GetValueOrDefault(false) && !requestData.HttpCompression) - { - message.Content = new ByteArrayContent(requestData.PostData.WrittenBytes); + // the written bytes are uncompressed, so can only be used when http compression isn't used + if (requestData.PostData.DisableDirectStreaming.GetValueOrDefault(false) && !requestData.HttpCompression) + { + message.Content = new ByteArrayContent(requestData.PostData.WrittenBytes); #if DOTNETCORE_2_1_OR_HIGHER - await stream.DisposeAsync().ConfigureAwait(false); + await stream.DisposeAsync().ConfigureAwait(false); #else - stream.Dispose(); + stream.Dispose(); #endif - } - else - { - stream.Position = 0; - message.Content = new StreamContent(stream); - } + } + else + { + stream.Position = 0; + message.Content = new StreamContent(stream); + } - if (requestData.HttpCompression) - message.Content.Headers.ContentEncoding.Add("gzip"); + if (requestData.HttpCompression) + message.Content.Headers.ContentEncoding.Add("gzip"); - message.Content.Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); - } + message.Content.Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType); } + } - private static System.Net.Http.HttpMethod ConvertHttpMethod(HttpMethod httpMethod) + private static System.Net.Http.HttpMethod ConvertHttpMethod(HttpMethod httpMethod) + { + switch (httpMethod) { - switch (httpMethod) - { - case HttpMethod.GET: return System.Net.Http.HttpMethod.Get; - case HttpMethod.POST: return System.Net.Http.HttpMethod.Post; - case HttpMethod.PUT: return System.Net.Http.HttpMethod.Put; - case HttpMethod.DELETE: return System.Net.Http.HttpMethod.Delete; - case HttpMethod.HEAD: return System.Net.Http.HttpMethod.Head; - default: - throw new ArgumentException("Invalid value for HttpMethod", nameof(httpMethod)); - } + case HttpMethod.GET: return System.Net.Http.HttpMethod.Get; + case HttpMethod.POST: return System.Net.Http.HttpMethod.Post; + case HttpMethod.PUT: return System.Net.Http.HttpMethod.Put; + case HttpMethod.DELETE: return System.Net.Http.HttpMethod.Delete; + case HttpMethod.HEAD: return System.Net.Http.HttpMethod.Head; + default: + throw new ArgumentException("Invalid value for HttpMethod", nameof(httpMethod)); } + } - internal static int GetClientKey(RequestData requestData) + internal static int GetClientKey(RequestData requestData) + { + unchecked { - unchecked - { - var hashCode = requestData.RequestTimeout.GetHashCode(); - hashCode = (hashCode * 397) ^ requestData.HttpCompression.GetHashCode(); - hashCode = (hashCode * 397) ^ (requestData.ProxyAddress?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (requestData.ProxyUsername?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (requestData.ProxyPassword?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ requestData.DisableAutomaticProxyDetection.GetHashCode(); - return hashCode; - } + var hashCode = requestData.RequestTimeout.GetHashCode(); + hashCode = (hashCode * 397) ^ requestData.HttpCompression.GetHashCode(); + hashCode = (hashCode * 397) ^ (requestData.ProxyAddress?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (requestData.ProxyUsername?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (requestData.ProxyPassword?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ requestData.DisableAutomaticProxyDetection.GetHashCode(); + return hashCode; } - - /// Allows subclasses to hook into the parents dispose - protected virtual void DisposeManagedResources() => HttpClientFactory.Dispose(); } + + /// + protected override void DisposeManagedResources() => HttpClientFactory.Dispose(); } #endif diff --git a/src/Elastic.Transport/Components/TransportClient/HttpWebRequestTransportClient.cs b/src/Elastic.Transport/Components/TransportClient/HttpWebRequestTransportClient.cs index 899bb75..90a8b0e 100644 --- a/src/Elastic.Transport/Components/TransportClient/HttpWebRequestTransportClient.cs +++ b/src/Elastic.Transport/Components/TransportClient/HttpWebRequestTransportClient.cs @@ -16,65 +16,141 @@ using Elastic.Transport.Diagnostics; using Elastic.Transport.Extensions; -namespace Elastic.Transport -{ - /// - /// This provides an implementation that targets . - /// - /// On .NET full framework is an alias to this. - /// - /// - /// Do NOT use this class directly on .NET Core. is monkey patched - /// over HttpClient and does not reuse its instances of HttpClient - /// - /// +namespace Elastic.Transport; + +/// +/// This provides an implementation that targets . +/// +/// On .NET full framework is an alias to this. +/// +/// +/// Do NOT use this class directly on .NET Core. is monkey patched +/// over HttpClient and does not reuse its instances of HttpClient +/// +/// #if DOTNETCORE - [Obsolete("CoreFX HttpWebRequest uses HttpClient under the covers but does not reuse HttpClient instances, do NOT use on .NET core only used as the default on Full Framework")] +[Obsolete("CoreFX HttpWebRequest uses HttpClient under the covers but does not reuse HttpClient instances, do NOT use on .NET core only used as the default on Full Framework")] #endif - public class HttpWebRequestTransportClient : ITransportClient +public class HttpWebRequestTransportClient : TransportClient +{ + private string _expectedCertificateFingerprint; + + static HttpWebRequestTransportClient() { - private string _expectedCertificateFingerprint; + //Not available under mono + if (!IsMono) HttpWebRequest.DefaultMaximumErrorResponseLength = -1; + } + + internal HttpWebRequestTransportClient() { } + + internal static bool IsMono { get; } = Type.GetType("Mono.Runtime") != null; - static HttpWebRequestTransportClient() + /// > + public override TResponse Request(RequestData requestData) + { + int? statusCode = null; + Stream responseStream = null; + Exception ex = null; + string mimeType = null; + long contentLength = -1; + ReadOnlyDictionary tcpStats = null; + ReadOnlyDictionary threadPoolStats = null; + Dictionary> responseHeaders = null; + + try { - //Not available under mono - if (!IsMono) HttpWebRequest.DefaultMaximumErrorResponseLength = -1; + var request = CreateHttpWebRequest(requestData); + var data = requestData.PostData; + + if (data != null) + { + using (var stream = request.GetRequestStream()) + { + if (requestData.HttpCompression) + { + using var zipStream = new GZipStream(stream, CompressionMode.Compress); + data.Write(zipStream, requestData.ConnectionSettings); + } + else + data.Write(stream, requestData.ConnectionSettings); + } + } + requestData.MadeItToResponse = true; + + if (requestData.TcpStats) + tcpStats = TcpStats.GetStates(); + + if (requestData.ThreadPoolStats) + threadPoolStats = ThreadPoolStats.GetStats(); + + //http://msdn.microsoft.com/en-us/library/system.net.httpwebresponse.getresponsestream.aspx + //Either the stream or the response object needs to be closed but not both although it won't + //throw any errors if both are closed atleast one of them has to be Closed. + //Since we expose the stream we let closing the stream determining when to close the connection + var httpWebResponse = (HttpWebResponse)request.GetResponse(); + HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); + responseHeaders = ParseHeaders(requestData, httpWebResponse, responseHeaders); + contentLength = httpWebResponse.ContentLength; + } + catch (WebException e) + { + ex = e; + if (e.Response is HttpWebResponse httpWebResponse) + HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); } - internal static bool IsMono { get; } = Type.GetType("Mono.Runtime") != null; + responseStream ??= Stream.Null; + var response = requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, ex, statusCode, responseHeaders, responseStream, mimeType, contentLength, threadPoolStats, tcpStats); + return response; + } - /// > - public virtual TResponse Request(RequestData requestData) - where TResponse : class, ITransportResponse, new() + /// > + public override async Task RequestAsync(RequestData requestData, + CancellationToken cancellationToken) + { + Action unregisterWaitHandle = null; + int? statusCode = null; + Stream responseStream = null; + Exception ex = null; + string mimeType = null; + long contentLength = -1; + ReadOnlyDictionary tcpStats = null; + ReadOnlyDictionary threadPoolStats = null; + Dictionary> responseHeaders = null; + requestData.IsAsync = true; + + try { - int? statusCode = null; - Stream responseStream = null; - Exception ex = null; - string mimeType = null; - long contentLength = -1; - ReadOnlyDictionary tcpStats = null; - ReadOnlyDictionary threadPoolStats = null; - Dictionary> responseHeaders = null; - - try + var data = requestData.PostData; + var request = CreateHttpWebRequest(requestData); + using (cancellationToken.Register(() => request.Abort())) { - var request = CreateHttpWebRequest(requestData); - var data = requestData.PostData; - if (data != null) { - using (var stream = request.GetRequestStream()) + var apmGetRequestStreamTask = + Task.Factory.FromAsync(request.BeginGetRequestStream, r => request.EndGetRequestStream(r), null); + unregisterWaitHandle = RegisterApmTaskTimeout(apmGetRequestStreamTask, request, requestData); + + using (var stream = await apmGetRequestStreamTask.ConfigureAwait(false)) { if (requestData.HttpCompression) { using var zipStream = new GZipStream(stream, CompressionMode.Compress); - data.Write(zipStream, requestData.ConnectionSettings); + await data.WriteAsync(zipStream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); } else - data.Write(stream, requestData.ConnectionSettings); + await data.WriteAsync(stream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); } + unregisterWaitHandle?.Invoke(); } requestData.MadeItToResponse = true; + //http://msdn.microsoft.com/en-us/library/system.net.httpwebresponse.getresponsestream.aspx + //Either the stream or the response object needs to be closed but not both although it won't + //throw any errors if both are closed atleast one of them has to be Closed. + //Since we expose the stream we let closing the stream determining when to close the connection + + var apmGetResponseTask = Task.Factory.FromAsync(request.BeginGetResponse, r => request.EndGetResponse(r), null); + unregisterWaitHandle = RegisterApmTaskTimeout(apmGetResponseTask, request, requestData); if (requestData.TcpStats) tcpStats = TcpStats.GetStates(); @@ -82,369 +158,286 @@ public virtual TResponse Request(RequestData requestData) if (requestData.ThreadPoolStats) threadPoolStats = ThreadPoolStats.GetStats(); - //http://msdn.microsoft.com/en-us/library/system.net.httpwebresponse.getresponsestream.aspx - //Either the stream or the response object needs to be closed but not both although it won't - //throw any errors if both are closed atleast one of them has to be Closed. - //Since we expose the stream we let closing the stream determining when to close the connection - var httpWebResponse = (HttpWebResponse)request.GetResponse(); + var httpWebResponse = (HttpWebResponse)await apmGetResponseTask.ConfigureAwait(false); HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); responseHeaders = ParseHeaders(requestData, httpWebResponse, responseHeaders); contentLength = httpWebResponse.ContentLength; } - catch (WebException e) - { - ex = e; - if (e.Response is HttpWebResponse httpWebResponse) - HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); - } - - responseStream ??= Stream.Null; - var response = requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, ex, statusCode, responseHeaders, responseStream, mimeType, contentLength, threadPoolStats, tcpStats); - return response; } - - /// > - public virtual async Task RequestAsync(RequestData requestData, - CancellationToken cancellationToken - ) - where TResponse : class, ITransportResponse, new() + catch (WebException e) { - Action unregisterWaitHandle = null; - int? statusCode = null; - Stream responseStream = null; - Exception ex = null; - string mimeType = null; - long contentLength = -1; - ReadOnlyDictionary tcpStats = null; - ReadOnlyDictionary threadPoolStats = null; - Dictionary> responseHeaders = null; - requestData.IsAsync = true; - - try - { - var data = requestData.PostData; - var request = CreateHttpWebRequest(requestData); - using (cancellationToken.Register(() => request.Abort())) - { - if (data != null) - { - var apmGetRequestStreamTask = - Task.Factory.FromAsync(request.BeginGetRequestStream, r => request.EndGetRequestStream(r), null); - unregisterWaitHandle = RegisterApmTaskTimeout(apmGetRequestStreamTask, request, requestData); - - using (var stream = await apmGetRequestStreamTask.ConfigureAwait(false)) - { - if (requestData.HttpCompression) - { - using var zipStream = new GZipStream(stream, CompressionMode.Compress); - await data.WriteAsync(zipStream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); - } - else - await data.WriteAsync(stream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); - } - unregisterWaitHandle?.Invoke(); - } - requestData.MadeItToResponse = true; - //http://msdn.microsoft.com/en-us/library/system.net.httpwebresponse.getresponsestream.aspx - //Either the stream or the response object needs to be closed but not both although it won't - //throw any errors if both are closed atleast one of them has to be Closed. - //Since we expose the stream we let closing the stream determining when to close the connection - - var apmGetResponseTask = Task.Factory.FromAsync(request.BeginGetResponse, r => request.EndGetResponse(r), null); - unregisterWaitHandle = RegisterApmTaskTimeout(apmGetResponseTask, request, requestData); - - if (requestData.TcpStats) - tcpStats = TcpStats.GetStates(); + ex = e; + if (e.Response is HttpWebResponse httpWebResponse) + HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); + } + finally + { + unregisterWaitHandle?.Invoke(); + } + responseStream ??= Stream.Null; + var response = await requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponseAsync + (requestData, ex, statusCode, responseHeaders, responseStream, mimeType, contentLength, threadPoolStats, tcpStats, cancellationToken) + .ConfigureAwait(false); + return response; + } - if (requestData.ThreadPoolStats) - threadPoolStats = ThreadPoolStats.GetStats(); + private static Dictionary> ParseHeaders(RequestData requestData, HttpWebResponse responseMessage, Dictionary> responseHeaders) + { + if (!responseMessage.SupportsHeaders && !responseMessage.Headers.HasKeys()) return null; - var httpWebResponse = (HttpWebResponse)await apmGetResponseTask.ConfigureAwait(false); - HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); - responseHeaders = ParseHeaders(requestData, httpWebResponse, responseHeaders); - contentLength = httpWebResponse.ContentLength; - } - } - catch (WebException e) - { - ex = e; - if (e.Response is HttpWebResponse httpWebResponse) - HandleResponse(httpWebResponse, out statusCode, out responseStream, out mimeType); - } - finally + if (requestData.ParseAllHeaders) + { + foreach (var key in responseMessage.Headers.AllKeys) { - unregisterWaitHandle?.Invoke(); + responseHeaders ??= new Dictionary>(); + responseHeaders.Add(key, responseMessage.Headers.GetValues(key)); } - responseStream ??= Stream.Null; - var response = await requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponseAsync - (requestData, ex, statusCode, responseHeaders, responseStream, mimeType, contentLength, threadPoolStats, tcpStats, cancellationToken) - .ConfigureAwait(false); - return response; } - - private static Dictionary> ParseHeaders(RequestData requestData, HttpWebResponse responseMessage, Dictionary> responseHeaders) + else if (requestData.ResponseHeadersToParse.Count > 0) { - if (!responseMessage.SupportsHeaders && !responseMessage.Headers.HasKeys()) return null; - - if (requestData.ParseAllHeaders) + foreach (var headerToParse in requestData.ResponseHeadersToParse) { - foreach (var key in responseMessage.Headers.AllKeys) + if (responseMessage.Headers.AllKeys.Contains(headerToParse, StringComparer.OrdinalIgnoreCase)) { responseHeaders ??= new Dictionary>(); - responseHeaders.Add(key, responseMessage.Headers.GetValues(key)); - } - } - else if (requestData.ResponseHeadersToParse.Count > 0) - { - foreach (var headerToParse in requestData.ResponseHeadersToParse) - { - if (responseMessage.Headers.AllKeys.Contains(headerToParse, StringComparer.OrdinalIgnoreCase)) - { - responseHeaders ??= new Dictionary>(); - responseHeaders.Add(headerToParse, responseMessage.Headers.GetValues(headerToParse)); - } + responseHeaders.Add(headerToParse, responseMessage.Headers.GetValues(headerToParse)); } } - - return responseHeaders; } - void IDisposable.Dispose() => DisposeManagedResources(); + return responseHeaders; + } + + /// + /// Allows subclasses to modify the instance that is going to be used for the API call + /// + /// An instance of describing where and how to call out to + internal HttpWebRequest CreateHttpWebRequest(RequestData requestData) + { + var request = CreateWebRequest(requestData); + SetAuthenticationIfNeeded(requestData, request); + SetProxyIfNeeded(request, requestData); + SetServerCertificateValidationCallBackIfNeeded(request, requestData); + SetClientCertificates(request, requestData); + AlterServicePoint(request.ServicePoint, requestData); + return request; + } + + /// Hook for subclasses to set additional client certificates on + internal void SetClientCertificates(HttpWebRequest request, RequestData requestData) + { + if (requestData.ClientCertificates != null) + request.ClientCertificates.AddRange(requestData.ClientCertificates); + } - /// - /// Allows subclasses to modify the instance that is going to be used for the API call - /// - /// An instance of describing where and how to call out to - protected virtual HttpWebRequest CreateHttpWebRequest(RequestData requestData) + private string ComparableFingerprint(string fingerprint) + { + var finalFingerprint = fingerprint; + if (fingerprint.Contains(':')) { - var request = CreateWebRequest(requestData); - SetAuthenticationIfNeeded(requestData, request); - SetProxyIfNeeded(request, requestData); - SetServerCertificateValidationCallBackIfNeeded(request, requestData); - SetClientCertificates(request, requestData); - AlterServicePoint(request.ServicePoint, requestData); - return request; + finalFingerprint = fingerprint.Replace(":", string.Empty); } - - /// Hook for subclasses to set additional client certificates on - protected virtual void SetClientCertificates(HttpWebRequest request, RequestData requestData) + else if (fingerprint.Contains('-')) { - if (requestData.ClientCertificates != null) - request.ClientCertificates.AddRange(requestData.ClientCertificates); + finalFingerprint = fingerprint.Replace("-", string.Empty); } + return finalFingerprint; + } - private string ComparableFingerprint(string fingerprint) + /// Hook for subclasses override the certificate validation on + internal void SetServerCertificateValidationCallBackIfNeeded(HttpWebRequest request, RequestData requestData) + { + var callback = requestData?.ConnectionSettings?.ServerCertificateValidationCallback; +#if !__MonoCS__ + //Only assign if one is defined on connection settings and a subclass has not already set one + if (callback != null && request.ServerCertificateValidationCallback == null) { - var finalFingerprint = fingerprint; - if (fingerprint.Contains(':')) - { - finalFingerprint = fingerprint.Replace(":", string.Empty); - } - else if (fingerprint.Contains('-')) - { - finalFingerprint = fingerprint.Replace("-", string.Empty); - } - return finalFingerprint; + request.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(callback); } - - /// Hook for subclasses override the certificate validation on - protected virtual void SetServerCertificateValidationCallBackIfNeeded(HttpWebRequest request, RequestData requestData) + else if (!string.IsNullOrEmpty(requestData.ConnectionSettings.CertificateFingerprint)) { - var callback = requestData?.ConnectionSettings?.ServerCertificateValidationCallback; -#if !__MonoCS__ - //Only assign if one is defined on connection settings and a subclass has not already set one - if (callback != null && request.ServerCertificateValidationCallback == null) + request.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback((request, certificate, chain, policyErrors) => { - request.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(callback); - } - else if (!string.IsNullOrEmpty(requestData.ConnectionSettings.CertificateFingerprint)) - { - request.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback((request, certificate, chain, policyErrors) => - { - if (certificate is null && chain is null) return false; + if (certificate is null && chain is null) return false; - // The "cleaned", expected fingerprint is cached to avoid repeated cost of converting it to a comparable form. - _expectedCertificateFingerprint ??= CertificateHelpers.ComparableFingerprint(requestData.ConnectionSettings.CertificateFingerprint); + // The "cleaned", expected fingerprint is cached to avoid repeated cost of converting it to a comparable form. + _expectedCertificateFingerprint ??= CertificateHelpers.ComparableFingerprint(requestData.ConnectionSettings.CertificateFingerprint); - // If there is a chain, check each certificate up to the root - if (chain is not null) + // If there is a chain, check each certificate up to the root + if (chain is not null) + { + foreach (var element in chain.ChainElements) { - foreach (var element in chain.ChainElements) - { - if (CertificateHelpers.ValidateCertificateFingerprint(element.Certificate, _expectedCertificateFingerprint)) - return true; - } + if (CertificateHelpers.ValidateCertificateFingerprint(element.Certificate, _expectedCertificateFingerprint)) + return true; } + } - // Otherwise, check the certificate - return CertificateHelpers.ValidateCertificateFingerprint(certificate, _expectedCertificateFingerprint); - }); - } + // Otherwise, check the certificate + return CertificateHelpers.ValidateCertificateFingerprint(certificate, _expectedCertificateFingerprint); + }); + } #else - if (callback != null) - throw new Exception("Mono misses ServerCertificateValidationCallback on HttpWebRequest"); + if (callback != null) + throw new Exception("Mono misses ServerCertificateValidationCallback on HttpWebRequest"); #endif - } + } - private static HttpWebRequest CreateWebRequest(RequestData requestData) - { - var request = (HttpWebRequest)WebRequest.Create(requestData.Uri); + private static HttpWebRequest CreateWebRequest(RequestData requestData) + { + var request = (HttpWebRequest)WebRequest.Create(requestData.Uri); - request.Accept = requestData.Accept; - request.ContentType = requestData.RequestMimeType; + request.Accept = requestData.Accept; + request.ContentType = requestData.RequestMimeType; #if !DOTNETCORE - // on netstandard/netcoreapp2.0 this throws argument exception - request.MaximumResponseHeadersLength = -1; + // on netstandard/netcoreapp2.0 this throws argument exception + request.MaximumResponseHeadersLength = -1; #endif - request.Pipelined = requestData.Pipelined; + request.Pipelined = requestData.Pipelined; - if (requestData.TransferEncodingChunked) - request.SendChunked = true; + if (requestData.TransferEncodingChunked) + request.SendChunked = true; - if (requestData.HttpCompression) - { - request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - request.Headers.Add("Accept-Encoding", "gzip,deflate"); - request.Headers.Add("Content-Encoding", "gzip"); - } + if (requestData.HttpCompression) + { + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + request.Headers.Add("Accept-Encoding", "gzip,deflate"); + request.Headers.Add("Content-Encoding", "gzip"); + } - var userAgent = requestData.UserAgent?.ToString(); - if (!string.IsNullOrWhiteSpace(userAgent)) - request.UserAgent = userAgent; + var userAgent = requestData.UserAgent?.ToString(); + if (!string.IsNullOrWhiteSpace(userAgent)) + request.UserAgent = userAgent; - if (!string.IsNullOrWhiteSpace(requestData.RunAs)) - request.Headers.Add(RequestData.RunAsSecurityHeader, requestData.RunAs); + if (!string.IsNullOrWhiteSpace(requestData.RunAs)) + request.Headers.Add(RequestData.RunAsSecurityHeader, requestData.RunAs); - if (requestData.Headers != null && requestData.Headers.HasKeys()) - request.Headers.Add(requestData.Headers); + if (requestData.Headers != null && requestData.Headers.HasKeys()) + request.Headers.Add(requestData.Headers); - if (requestData.MetaHeaderProvider is object) - { - var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData); + if (requestData.MetaHeaderProvider is object) + { + var value = requestData.MetaHeaderProvider.ProduceHeaderValue(requestData); - if (!string.IsNullOrEmpty(value)) - request.Headers.Add(requestData.MetaHeaderProvider.HeaderName, requestData.MetaHeaderProvider.ProduceHeaderValue(requestData)); - } + if (!string.IsNullOrEmpty(value)) + request.Headers.Add(requestData.MetaHeaderProvider.HeaderName, requestData.MetaHeaderProvider.ProduceHeaderValue(requestData)); + } - var timeout = (int)requestData.RequestTimeout.TotalMilliseconds; - request.Timeout = timeout; - request.ReadWriteTimeout = timeout; + var timeout = (int)requestData.RequestTimeout.TotalMilliseconds; + request.Timeout = timeout; + request.ReadWriteTimeout = timeout; - //WebRequest won't send Content-Length: 0 for empty bodies - //which goes against RFC's and might break i.e IIS when used as a proxy. - //see: https://github.com/elastic/elasticsearch-net/issues/562 - var m = requestData.Method.GetStringValue(); - request.Method = m; - if (m != "HEAD" && m != "GET" && requestData.PostData == null) - request.ContentLength = 0; + //WebRequest won't send Content-Length: 0 for empty bodies + //which goes against RFC's and might break i.e IIS when used as a proxy. + //see: https://github.com/elastic/elasticsearch-net/issues/562 + var m = requestData.Method.GetStringValue(); + request.Method = m; + if (m != "HEAD" && m != "GET" && requestData.PostData == null) + request.ContentLength = 0; - return request; - } + return request; + } - /// Hook for subclasses override behavior - protected virtual void AlterServicePoint(ServicePoint requestServicePoint, RequestData requestData) - { - requestServicePoint.UseNagleAlgorithm = false; - requestServicePoint.Expect100Continue = false; - requestServicePoint.ConnectionLeaseTimeout = (int)requestData.DnsRefreshTimeout.TotalMilliseconds; - if (requestData.ConnectionSettings.ConnectionLimit > 0) - requestServicePoint.ConnectionLimit = requestData.ConnectionSettings.ConnectionLimit; - //looking at http://referencesource.microsoft.com/#System/net/System/Net/ServicePoint.cs - //this method only sets internal values and wont actually cause timers and such to be reset - //So it should be idempotent if called with the same parameters - requestServicePoint.SetTcpKeepAlive(true, requestData.KeepAliveTime, requestData.KeepAliveInterval); - } + /// Hook for subclasses override behavior + internal void AlterServicePoint(ServicePoint requestServicePoint, RequestData requestData) + { + requestServicePoint.UseNagleAlgorithm = false; + requestServicePoint.Expect100Continue = false; + requestServicePoint.ConnectionLeaseTimeout = (int)requestData.DnsRefreshTimeout.TotalMilliseconds; + if (requestData.ConnectionSettings.ConnectionLimit > 0) + requestServicePoint.ConnectionLimit = requestData.ConnectionSettings.ConnectionLimit; + //looking at http://referencesource.microsoft.com/#System/net/System/Net/ServicePoint.cs + //this method only sets internal values and wont actually cause timers and such to be reset + //So it should be idempotent if called with the same parameters + requestServicePoint.SetTcpKeepAlive(true, requestData.KeepAliveTime, requestData.KeepAliveInterval); + } - /// Hook for subclasses to set proxy on - protected virtual void SetProxyIfNeeded(HttpWebRequest request, RequestData requestData) + /// Hook for subclasses to set proxy on + internal void SetProxyIfNeeded(HttpWebRequest request, RequestData requestData) + { + if (!string.IsNullOrWhiteSpace(requestData.ProxyAddress)) { - if (!string.IsNullOrWhiteSpace(requestData.ProxyAddress)) - { - var uri = new Uri(requestData.ProxyAddress); - var proxy = new WebProxy(uri); - var credentials = new NetworkCredential(requestData.ProxyUsername, requestData.ProxyPassword); - proxy.Credentials = credentials; - request.Proxy = proxy; - } - - if (requestData.DisableAutomaticProxyDetection) - request.Proxy = null!; + var uri = new Uri(requestData.ProxyAddress); + var proxy = new WebProxy(uri); + var credentials = new NetworkCredential(requestData.ProxyUsername, requestData.ProxyPassword); + proxy.Credentials = credentials; + request.Proxy = proxy; } - /// Hook for subclasses to set authentication on - protected virtual void SetAuthenticationIfNeeded(RequestData requestData, HttpWebRequest request) - { - //If user manually specifies an Authorization Header give it preference - if (requestData.Headers.HasKeys() && requestData.Headers.AllKeys.Contains("Authorization")) - { - var header = requestData.Headers["Authorization"]; - request.Headers["Authorization"] = header; - return; - } - SetBasicAuthenticationIfNeeded(request, requestData); - } + if (requestData.DisableAutomaticProxyDetection) + request.Proxy = null!; + } - private static void SetBasicAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData) + /// Hook for subclasses to set authentication on + internal void SetAuthenticationIfNeeded(RequestData requestData, HttpWebRequest request) + { + //If user manually specifies an Authorization Header give it preference + if (requestData.Headers.HasKeys() && requestData.Headers.AllKeys.Contains("Authorization")) { - // Basic auth credentials take the following precedence (highest -> lowest): - // 1 - Specified on the request (highest precedence) - // 2 - Specified at the global ITransportClientSettings level - // 3 - Specified with the URI (lowest precedence) - + var header = requestData.Headers["Authorization"]; + request.Headers["Authorization"] = header; + return; + } + SetBasicAuthenticationIfNeeded(request, requestData); + } - // Basic auth credentials take the following precedence (highest -> lowest): - // 1 - Specified with the URI (highest precedence) - // 2 - Specified on the request - // 3 - Specified at the global ITransportClientSettings level (lowest precedence) + private static void SetBasicAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData) + { + // Basic auth credentials take the following precedence (highest -> lowest): + // 1 - Specified on the request (highest precedence) + // 2 - Specified at the global TransportClientSettings level + // 3 - Specified with the URI (lowest precedence) - string parameters = null; - string scheme = null; - if (!requestData.Uri.UserInfo.IsNullOrEmpty()) - { - parameters = BasicAuthentication.GetBase64String(Uri.UnescapeDataString(requestData.Uri.UserInfo)); - scheme = BasicAuthentication.BasicAuthenticationScheme; - } - else if (requestData.AuthenticationHeader != null && requestData.AuthenticationHeader.TryGetAuthorizationParameters(out var v)) - { - parameters = v; - scheme = requestData.AuthenticationHeader.AuthScheme; - } - if (parameters.IsNullOrEmpty()) return; + // Basic auth credentials take the following precedence (highest -> lowest): + // 1 - Specified with the URI (highest precedence) + // 2 - Specified on the request + // 3 - Specified at the global TransportClientSettings level (lowest precedence) - request.Headers["Authorization"] = $"{scheme} {parameters}"; + string parameters = null; + string scheme = null; + if (!requestData.Uri.UserInfo.IsNullOrEmpty()) + { + parameters = BasicAuthentication.GetBase64String(Uri.UnescapeDataString(requestData.Uri.UserInfo)); + scheme = BasicAuthentication.BasicAuthenticationScheme; } - - /// - /// Registers an APM async task cancellation on the threadpool - /// - /// An unregister action that can be used to remove the waithandle prematurely - private static Action RegisterApmTaskTimeout(IAsyncResult result, WebRequest request, RequestData requestData) + else if (requestData.AuthenticationHeader != null && requestData.AuthenticationHeader.TryGetAuthorizationParameters(out var v)) { - var waitHandle = result.AsyncWaitHandle; - var registeredWaitHandle = - ThreadPool.RegisterWaitForSingleObject(waitHandle, TimeoutCallback, request, requestData.RequestTimeout, true); - return () => registeredWaitHandle.Unregister(waitHandle); + parameters = v; + scheme = requestData.AuthenticationHeader.AuthScheme; } - private static void TimeoutCallback(object state, bool timedOut) - { - if (!timedOut) return; + if (parameters.IsNullOrEmpty()) return; - (state as WebRequest)?.Abort(); - } + request.Headers["Authorization"] = $"{scheme} {parameters}"; + } - private static void HandleResponse(HttpWebResponse response, out int? statusCode, out Stream responseStream, out string mimeType) - { - statusCode = (int)response.StatusCode; - responseStream = response.GetResponseStream(); - mimeType = response.ContentType; - // https://github.com/elastic/elasticsearch-net/issues/2311 - // if stream is null call dispose on response instead. - if (responseStream == null || responseStream == Stream.Null) response.Dispose(); - } + /// + /// Registers an APM async task cancellation on the threadpool + /// + /// An unregister action that can be used to remove the waithandle prematurely + private static Action RegisterApmTaskTimeout(IAsyncResult result, WebRequest request, RequestData requestData) + { + var waitHandle = result.AsyncWaitHandle; + var registeredWaitHandle = + ThreadPool.RegisterWaitForSingleObject(waitHandle, TimeoutCallback, request, requestData.RequestTimeout, true); + return () => registeredWaitHandle.Unregister(waitHandle); + } - /// Allows subclasses to hook into the parents dispose - protected virtual void DisposeManagedResources() { } + private static void TimeoutCallback(object state, bool timedOut) + { + if (!timedOut) return; + + (state as WebRequest)?.Abort(); + } + + private static void HandleResponse(HttpWebResponse response, out int? statusCode, out Stream responseStream, out string mimeType) + { + statusCode = (int)response.StatusCode; + responseStream = response.GetResponseStream(); + mimeType = response.ContentType; + // https://github.com/elastic/elasticsearch-net/issues/2311 + // if stream is null call dispose on response instead. + if (responseStream == null || responseStream == Stream.Null) response.Dispose(); } } diff --git a/src/Elastic.Transport/Components/TransportClient/ITransportClient.cs b/src/Elastic.Transport/Components/TransportClient/ITransportClient.cs deleted file mode 100644 index 8140531..0000000 --- a/src/Elastic.Transport/Components/TransportClient/ITransportClient.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Elastic.Transport -{ - /// - /// This interface abstracts the actual IO performs. - /// holds a single instance of this class - /// The instance to be used is provided to the constructor of implementations - /// Where its exposed under - /// - public interface ITransportClient : IDisposable - { - /// - /// Perform a request to the endpoint described by using its associated configuration. - /// - /// An object describing where and how to perform the IO call - /// - /// - /// An implementation of ensuring enough information is available - /// for and to determine what to - /// do with the response - /// - /// - /// An implementation of ensuring enough information is available - /// for and to determine what to - /// do with the response - /// - Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) - where TResponse : class, ITransportResponse, new(); - - /// - /// Perform a request to the endpoint described by using its associated configuration. - /// - /// An object describing where and how to perform the IO call - /// - /// An implementation of ensuring enough information is available - /// for and to determine what to - /// do with the response - /// - /// - /// An implementation of ensuring enough information is available - /// for and to determine what to - /// do with the response - /// - TResponse Request(RequestData requestData) - where TResponse : class, ITransportResponse, new(); - } -} diff --git a/src/Elastic.Transport/Components/TransportClient/InMemoryTransportClient.cs b/src/Elastic.Transport/Components/TransportClient/InMemoryTransportClient.cs index c747c45..51ea01a 100644 --- a/src/Elastic.Transport/Components/TransportClient/InMemoryTransportClient.cs +++ b/src/Elastic.Transport/Components/TransportClient/InMemoryTransportClient.cs @@ -10,112 +10,104 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// An implementation of designed to not actually do any IO and services requests from an in memory byte buffer +/// +public class InMemoryConnection : TransportClient { + private static readonly byte[] EmptyBody = Encoding.UTF8.GetBytes(""); + private readonly string _contentType; + private readonly Exception _exception; + private readonly byte[] _responseBody; + private readonly int _statusCode; + private readonly Dictionary> _headers; + /// - /// An implementation of designed to not actually do any IO and services requests from an in memory byte buffer + /// Every request will succeed with this overload, note that it won't actually return mocked responses + /// so using this overload might fail if you are using it to test high level bits that need to deserialize the response. /// - public class InMemoryConnection : ITransportClient - { - private static readonly byte[] EmptyBody = Encoding.UTF8.GetBytes(""); - private readonly string _contentType; - private readonly Exception _exception; - private readonly byte[] _responseBody; - private readonly int _statusCode; - private readonly Dictionary> _headers; + public InMemoryConnection() => _statusCode = 200; - /// - /// Every request will succeed with this overload, note that it won't actually return mocked responses - /// so using this overload might fail if you are using it to test high level bits that need to deserialize the response. - /// - public InMemoryConnection() => _statusCode = 200; - - /// - public InMemoryConnection(byte[] responseBody, int statusCode = 200, Exception exception = null, string contentType = RequestData.MimeType, Dictionary> headers = null) - { - _responseBody = responseBody; - _statusCode = statusCode; - _exception = exception; - _contentType = contentType; - _headers = headers; - } - - /// > - public virtual TResponse Request(RequestData requestData) - where TResponse : class, ITransportResponse, new() => - ReturnConnectionStatus(requestData); + /// + public InMemoryConnection(byte[] responseBody, int statusCode = 200, Exception exception = null, string contentType = RequestData.MimeType, Dictionary> headers = null) + { + _responseBody = responseBody; + _statusCode = statusCode; + _exception = exception; + _contentType = contentType; + _headers = headers; + } - /// > - public virtual Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) - where TResponse : class, ITransportResponse, new() => - ReturnConnectionStatusAsync(requestData, cancellationToken); + /// > + public override TResponse Request(RequestData requestData) => + ReturnConnectionStatus(requestData); - void IDisposable.Dispose() => DisposeManagedResources(); + /// > + public override Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) => + ReturnConnectionStatusAsync(requestData, cancellationToken); - /// - /// Allow subclasses to provide their own implementations for while reusing the more complex logic - /// to create a response - /// - /// An instance of describing where and how to call out to - /// The bytes intended to be used as return - /// The status code that the responses should return - /// - protected TResponse ReturnConnectionStatus(RequestData requestData, byte[] responseBody = null, int? statusCode = null, - string contentType = null) - where TResponse : class, ITransportResponse, new() + /// + /// Allow subclasses to provide their own implementations for while reusing the more complex logic + /// to create a response + /// + /// An instance of describing where and how to call out to + /// The bytes intended to be used as return + /// The status code that the responses should return + /// + internal TResponse ReturnConnectionStatus(RequestData requestData, byte[] responseBody = null, int? statusCode = null, + string contentType = null) + where TResponse : TransportResponse, new() + { + var body = responseBody ?? _responseBody; + var data = requestData.PostData; + if (data != null) { - var body = responseBody ?? _responseBody; - var data = requestData.PostData; - if (data != null) + using (var stream = requestData.MemoryStreamFactory.Create()) { - using (var stream = requestData.MemoryStreamFactory.Create()) + if (requestData.HttpCompression) { - if (requestData.HttpCompression) - { - using var zipStream = new GZipStream(stream, CompressionMode.Compress); - data.Write(zipStream, requestData.ConnectionSettings); - } - else - data.Write(stream, requestData.ConnectionSettings); + using var zipStream = new GZipStream(stream, CompressionMode.Compress); + data.Write(zipStream, requestData.ConnectionSettings); } + else + data.Write(stream, requestData.ConnectionSettings); } - requestData.MadeItToResponse = true; - - var sc = statusCode ?? _statusCode; - Stream s = body != null ? requestData.MemoryStreamFactory.Create(body) : requestData.MemoryStreamFactory.Create(EmptyBody); - return requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, _exception, sc, _headers, s, contentType ?? _contentType ?? RequestData.MimeType, body?.Length ?? 0, null, null); } + requestData.MadeItToResponse = true; - /// > - protected async Task ReturnConnectionStatusAsync(RequestData requestData, CancellationToken cancellationToken, - byte[] responseBody = null, int? statusCode = null, string contentType = null) - where TResponse : class, ITransportResponse, new() + var sc = statusCode ?? _statusCode; + Stream s = body != null ? requestData.MemoryStreamFactory.Create(body) : requestData.MemoryStreamFactory.Create(EmptyBody); + return requestData.ConnectionSettings.ProductRegistration.ResponseBuilder.ToResponse(requestData, _exception, sc, _headers, s, contentType ?? _contentType ?? RequestData.MimeType, body?.Length ?? 0, null, null); + } + + /// > + internal async Task ReturnConnectionStatusAsync(RequestData requestData, CancellationToken cancellationToken, + byte[] responseBody = null, int? statusCode = null, string contentType = null) + where TResponse : TransportResponse, new() + { + var body = responseBody ?? _responseBody; + var data = requestData.PostData; + if (data != null) { - var body = responseBody ?? _responseBody; - var data = requestData.PostData; - if (data != null) + using (var stream = requestData.MemoryStreamFactory.Create()) { - using (var stream = requestData.MemoryStreamFactory.Create()) + if (requestData.HttpCompression) { - if (requestData.HttpCompression) - { - using var zipStream = new GZipStream(stream, CompressionMode.Compress); - await data.WriteAsync(zipStream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); - } - else - await data.WriteAsync(stream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); + using var zipStream = new GZipStream(stream, CompressionMode.Compress); + await data.WriteAsync(zipStream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); } + else + await data.WriteAsync(stream, requestData.ConnectionSettings, cancellationToken).ConfigureAwait(false); } - requestData.MadeItToResponse = true; - - var sc = statusCode ?? _statusCode; - Stream s = body != null ? requestData.MemoryStreamFactory.Create(body) : requestData.MemoryStreamFactory.Create(EmptyBody); - return await requestData.ConnectionSettings.ProductRegistration.ResponseBuilder - .ToResponseAsync(requestData, _exception, sc, _headers, s, contentType ?? _contentType, body?.Length ?? 0, null, null, cancellationToken) - .ConfigureAwait(false); } + requestData.MadeItToResponse = true; - /// Allows subclasses to hook into the parents dispose - protected virtual void DisposeManagedResources() { } + var sc = statusCode ?? _statusCode; + Stream s = body != null ? requestData.MemoryStreamFactory.Create(body) : requestData.MemoryStreamFactory.Create(EmptyBody); + return await requestData.ConnectionSettings.ProductRegistration.ResponseBuilder + .ToResponseAsync(requestData, _exception, sc, _headers, s, contentType ?? _contentType, body?.Length ?? 0, null, null, cancellationToken) + .ConfigureAwait(false); } } diff --git a/src/Elastic.Transport/Components/TransportClient/TransportClient.cs b/src/Elastic.Transport/Components/TransportClient/TransportClient.cs new file mode 100644 index 0000000..0ed4ac4 --- /dev/null +++ b/src/Elastic.Transport/Components/TransportClient/TransportClient.cs @@ -0,0 +1,88 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Transport; + +/// +/// This interface abstracts the actual IO performs. +/// holds a single instance of this class +/// The instance to be used is provided to the constructor of implementations +/// Where its exposed under +/// +public abstract class TransportClient : IDisposable +{ + private bool _disposed = false; + + internal TransportClient() { } + + /// + /// Perform a request to the endpoint described by using its associated configuration. + /// + /// An object describing where and how to perform the IO call + /// + /// + /// An implementation of ensuring enough information is available + /// for and to determine what to + /// do with the response + /// + /// + /// An implementation of ensuring enough information is available + /// for and to determine what to + /// do with the response + /// + public abstract Task RequestAsync(RequestData requestData, CancellationToken cancellationToken) + where TResponse : TransportResponse, new(); + + /// + /// Perform a request to the endpoint described by using its associated configuration. + /// + /// An object describing where and how to perform the IO call + /// + /// An implementation of ensuring enough information is available + /// for and to determine what to + /// do with the response + /// + /// + /// An implementation of ensuring enough information is available + /// for and to determine what to + /// do with the response + /// + public abstract TResponse Request(RequestData requestData) + where TResponse : TransportResponse, new(); + + /// + /// + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// + /// + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + DisposeManagedResources(); + } + + _disposed = true; + } + + /// + /// + /// + protected virtual void DisposeManagedResources() { } +} diff --git a/src/Elastic.Transport/Components/TransportClient/WebProxy.cs b/src/Elastic.Transport/Components/TransportClient/WebProxy.cs new file mode 100644 index 0000000..3f3ca20 --- /dev/null +++ b/src/Elastic.Transport/Components/TransportClient/WebProxy.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +#if DOTNETCORE +using System; +using System.Net; + +namespace Elastic.Transport; + +internal class WebProxy : IWebProxy +{ + private readonly Uri _uri; + + public WebProxy(Uri uri) => _uri = uri; + + public ICredentials Credentials { get; set; } + + public Uri GetProxy(Uri destination) => _uri; + + public bool IsBypassed(Uri host) => host.IsLoopback; +} +#endif diff --git a/src/Elastic.Transport/Configuration/ConnectionInfo.cs b/src/Elastic.Transport/Configuration/ConnectionInfo.cs index c80cd20..1405686 100644 --- a/src/Elastic.Transport/Configuration/ConnectionInfo.cs +++ b/src/Elastic.Transport/Configuration/ConnectionInfo.cs @@ -7,45 +7,44 @@ using System.Net.Http; #endif -namespace Elastic.Transport -{ - internal static class ConnectionInfo - { - public static bool UsingCurlHandler +namespace Elastic.Transport; + +internal static class ConnectionInfo +{ + public static bool UsingCurlHandler + { + get { - get - { - // Not available after .NET 5.0 + // Not available after .NET 5.0 #if NET5_0_OR_GREATER || !DOTNETCORE #pragma warning disable IDE0025 // Use expression body for properties - return false; + return false; #pragma warning restore IDE0025 // Use expression body for properties #else - var curlHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null; - if (!curlHandlerExists) - return false; + var curlHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.CurlHandler") != null; + if (!curlHandlerExists) + return false; - var socketsHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler") != null; - // running on a .NET core version with CurlHandler, before the existence of SocketsHttpHandler. - // Must be using CurlHandler. - if (!socketsHandlerExists) - return true; + var socketsHandlerExists = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler") != null; + // running on a .NET core version with CurlHandler, before the existence of SocketsHttpHandler. + // Must be using CurlHandler. + if (!socketsHandlerExists) + return true; - if (AppContext.TryGetSwitch("System.Net.Http.UseSocketsHttpHandler", out var isEnabled)) - return !isEnabled; + if (AppContext.TryGetSwitch("System.Net.Http.UseSocketsHttpHandler", out var isEnabled)) + return !isEnabled; - var environmentVariable = - Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"); + var environmentVariable = + Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"); - // SocketsHandler exists and no environment variable exists to disable it. - // Must be using SocketsHandler and not CurlHandler - if (environmentVariable == null) - return false; + // SocketsHandler exists and no environment variable exists to disable it. + // Must be using SocketsHandler and not CurlHandler + if (environmentVariable == null) + return false; - return environmentVariable.Equals("false", StringComparison.OrdinalIgnoreCase) || - environmentVariable.Equals("0"); + return environmentVariable.Equals("false", StringComparison.OrdinalIgnoreCase) || + environmentVariable.Equals("0"); #endif - } } } } diff --git a/src/Elastic.Transport/Configuration/HeadersList.cs b/src/Elastic.Transport/Configuration/HeadersList.cs index 0d82eca..3f56bb3 100644 --- a/src/Elastic.Transport/Configuration/HeadersList.cs +++ b/src/Elastic.Transport/Configuration/HeadersList.cs @@ -7,111 +7,110 @@ using System.Collections.Generic; using System.Linq; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Represents a unique, case-insensitive, immutable collection of header names. +/// +public readonly struct HeadersList : IEnumerable { + private readonly List _headers; + /// - /// Represents a unique, case-insensitive, immutable collection of header names. + /// Create a new from an existing enumerable of header names. + /// Duplicate names, including those which only differ by case, will be ignored. /// - public struct HeadersList : IEnumerable + /// The header names to initialise the with. + public HeadersList(IEnumerable headers) { - private readonly List _headers; - - /// - /// Create a new from an existing enumerable of header names. - /// Duplicate names, including those which only differ by case, will be ignored. - /// - /// The header names to initialise the with. - public HeadersList(IEnumerable headers) - { - _headers = new List(); + _headers = new List(); - foreach (var header in headers) + foreach (var header in headers) + { + if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) { - if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) - { - _headers.Add(header); - } + _headers.Add(header); } } + } - /// - /// - /// - /// - /// - public HeadersList(IEnumerable headers, string additionalHeader) - { - _headers = new List(); - - foreach (var header in headers) - { - if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) - { - _headers.Add(header); - } - } + /// + /// + /// + /// + /// + public HeadersList(IEnumerable headers, string additionalHeader) + { + _headers = new List(); - if (!_headers.Contains(additionalHeader, StringComparer.OrdinalIgnoreCase)) + foreach (var header in headers) + { + if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) { - _headers.Add(additionalHeader); + _headers.Add(header); } } - /// - /// - /// - /// - /// - public HeadersList(IEnumerable headers, IEnumerable otherHeaders) + if (!_headers.Contains(additionalHeader, StringComparer.OrdinalIgnoreCase)) { - _headers = new List(); + _headers.Add(additionalHeader); + } + } + + /// + /// + /// + /// + /// + public HeadersList(IEnumerable headers, IEnumerable otherHeaders) + { + _headers = new List(); - foreach (var header in headers) + foreach (var header in headers) + { + if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) { - if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) - { - _headers.Add(header); - } + _headers.Add(header); } + } - foreach (var header in otherHeaders) + foreach (var header in otherHeaders) + { + if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) { - if (!_headers.Contains(header, StringComparer.OrdinalIgnoreCase)) - { - _headers.Add(header); - } + _headers.Add(header); } } + } - /// - /// Create a new initialised with a single header name. - /// - /// The header name to initialise the with. - public HeadersList(string header) => _headers = new List { header }; + /// + /// Create a new initialised with a single header name. + /// + /// The header name to initialise the with. + public HeadersList(string header) => _headers = new List { header }; + + /// + /// Gets the number of elements contained in the . + /// + public int Count => _headers is null ? 0 : _headers.Count; - /// - /// Gets the number of elements contained in the . - /// - public int Count => _headers is null ? 0 : _headers.Count; + /// + public IEnumerator GetEnumerator() => _headers?.GetEnumerator() ?? (IEnumerator)new EmptyEnumerator(); - /// - public IEnumerator GetEnumerator() => _headers?.GetEnumerator() ?? (IEnumerator)new EmptyEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + internal struct EmptyEnumerator : IEnumerator + { + public T Current => default; + object IEnumerator.Current => Current; + public bool MoveNext() => false; - internal struct EmptyEnumerator : IEnumerator + public void Reset() { - public T Current => default; - object IEnumerator.Current => Current; - public bool MoveNext() => false; - - public void Reset() - { - } + } - public void Dispose() - { - } + public void Dispose() + { } } } diff --git a/src/Elastic.Transport/Configuration/ITransportConfiguration.cs b/src/Elastic.Transport/Configuration/ITransportConfiguration.cs index 68607a5..6ba5926 100644 --- a/src/Elastic.Transport/Configuration/ITransportConfiguration.cs +++ b/src/Elastic.Transport/Configuration/ITransportConfiguration.cs @@ -11,289 +11,288 @@ using System.Threading; using Elastic.Transport.Products; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// All the transport configuration that you as the user can use to steer the behavior of the and all the components such +/// as and . +/// +public interface ITransportConfiguration : IDisposable { + /// + AuthorizationHeader Authentication { get; } + + /// Provides a semaphoreslim to transport implementations that need to limit access to a resource + SemaphoreSlim BootstrapLock { get; } + + /// + /// Use the following certificates to authenticate all HTTP requests. You can also set them on individual + /// request using + /// + X509CertificateCollection ClientCertificates { get; } + + /// The connection abstraction behind which all actual IO happens + TransportClient Connection { get; } + + /// + /// Limits the number of concurrent connections that can be opened to an endpoint. Defaults to 80 (see + /// ). + /// + /// For Desktop CLR, this setting applies to the DefaultConnectionLimit property on the ServicePointManager object when creating + /// ServicePoint objects, affecting the default implementation. + /// + /// + /// For Core CLR, this setting applies to the MaxConnectionsPerServer property on the HttpClientHandler instances used by the HttpClient + /// inside the default implementation + /// + /// + int ConnectionLimit { get; } + + /// The connection pool to use when talking with Elasticsearch + NodePool NodePool { get; } + + /// + /// Returns information about the current product making use of the transport. + /// + ProductRegistration ProductRegistration { get; } + + /// + /// The time to put dead nodes out of rotation (this will be multiplied by the number of times they've been dead) + /// + TimeSpan? DeadTimeout { get; } + + /// + /// Disabled proxy detection on the webrequest, in some cases this may speed up the first connection + /// your appdomain makes, in other cases it will actually increase the time for the first connection. + /// No silver bullet! use with care! + /// + bool DisableAutomaticProxyDetection { get; } + + /// + /// When set to true will disable (de)serializing directly to the request and response stream and return a byte[] + /// copy of the raw request and response. Defaults to false + /// + bool DisableDirectStreaming { get; } + + /// + /// This signals that we do not want to send initial pings to unknown/previously dead nodes + /// and just send the call straightaway + /// + bool DisablePings { get; } + + /// + /// Enable gzip compressed requests and responses + /// + bool EnableHttpCompression { get; } + + /// + /// Try to send these headers for every request + /// + NameValueCollection Headers { get; } + + /// + /// Whether HTTP pipelining is enabled. The default is true + /// + bool HttpPipeliningEnabled { get; } + + /// + /// KeepAliveInterval - specifies the interval, in milliseconds, between + /// when successive keep-alive packets are sent if no acknowledgement is + /// received. + /// + TimeSpan? KeepAliveInterval { get; } + + /// + /// KeepAliveTime - specifies the timeout, in milliseconds, with no + /// activity until the first keep-alive packet is sent. + /// + TimeSpan? KeepAliveTime { get; } + + /// + /// The maximum amount of time a node is allowed to marked dead + /// + TimeSpan? MaxDeadTimeout { get; } + + /// + /// When a retryable exception occurs or status code is returned this controls the maximum + /// amount of times we should retry the call to Elasticsearch + /// + int? MaxRetries { get; } + + /// + /// Limits the total runtime including retries separately from + ///
+	/// When not specified defaults to  which itself defaults to 60 seconds
+	/// 
+ ///
+ TimeSpan? MaxRetryTimeout { get; } + + /// Provides a memory stream factory + MemoryStreamFactory MemoryStreamFactory { get; } + + /// + /// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and + /// always execute on all nodes. + /// When using an implementation that supports reseeding of nodes, this will default to omitting master only + /// node from regular API calls. + /// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken + /// verbatim. + /// + Func NodePredicate { get; } + + /// + /// Allows you to register a callback every time a an API call is returned + /// + Action OnRequestCompleted { get; } + + /// + /// An action to run when the for a request has been + /// created. + /// + Action OnRequestDataCreated { get; } + + /// + /// When enabled, all headers from the HTTP response will be included in the . + /// + bool? ParseAllHeaders { get; } + + /// + /// The timeout in milliseconds to use for ping requests, which are issued to determine whether a node is alive + /// + TimeSpan? PingTimeout { get; } + + /// + /// When set will force all connections through this proxy + /// + string ProxyAddress { get; } + + /// + /// The password for the proxy, when configured + /// + string ProxyPassword { get; } + + /// + /// The username for the proxy, when configured + /// + string ProxyUsername { get; } + + /// + /// Append these query string parameters automatically to every request + /// + NameValueCollection QueryStringParameters { get; } + + /// The serializer to use to serialize requests and deserialize responses + Serializer RequestResponseSerializer { get; } + + /// + /// The timeout in milliseconds for each request to Elasticsearch + /// + TimeSpan RequestTimeout { get; } + + /// + /// A containing the names of all HTTP response headers to attempt to parse and + /// included on the . + /// + HeadersList ResponseHeadersToParse { get; } + /// - /// All the transport configuration that you as the user can use to steer the behavior of the and all the components such - /// as and . - /// - public interface ITransportConfiguration : IDisposable - { - /// - AuthorizationHeader Authentication { get; } - - /// Provides a semaphoreslim to transport implementations that need to limit access to a resource - SemaphoreSlim BootstrapLock { get; } - - /// - /// Use the following certificates to authenticate all HTTP requests. You can also set them on individual - /// request using - /// - X509CertificateCollection ClientCertificates { get; } - - /// The connection abstraction behind which all actual IO happens - ITransportClient Connection { get; } - - /// - /// Limits the number of concurrent connections that can be opened to an endpoint. Defaults to 80 (see - /// ). - /// - /// For Desktop CLR, this setting applies to the DefaultConnectionLimit property on the ServicePointManager object when creating - /// ServicePoint objects, affecting the default implementation. - /// - /// - /// For Core CLR, this setting applies to the MaxConnectionsPerServer property on the HttpClientHandler instances used by the HttpClient - /// inside the default implementation - /// - /// - int ConnectionLimit { get; } - - /// The connection pool to use when talking with Elasticsearch - NodePool NodePool { get; } - - /// - /// Returns information about the current product making use of the transport. - /// - IProductRegistration ProductRegistration { get; } - - /// - /// The time to put dead nodes out of rotation (this will be multiplied by the number of times they've been dead) - /// - TimeSpan? DeadTimeout { get; } - - /// - /// Disabled proxy detection on the webrequest, in some cases this may speed up the first connection - /// your appdomain makes, in other cases it will actually increase the time for the first connection. - /// No silver bullet! use with care! - /// - bool DisableAutomaticProxyDetection { get; } - - /// - /// When set to true will disable (de)serializing directly to the request and response stream and return a byte[] - /// copy of the raw request and response. Defaults to false - /// - bool DisableDirectStreaming { get; } - - /// - /// This signals that we do not want to send initial pings to unknown/previously dead nodes - /// and just send the call straightaway - /// - bool DisablePings { get; } - - /// - /// Enable gzip compressed requests and responses - /// - bool EnableHttpCompression { get; } - - /// - /// Try to send these headers for every request - /// - NameValueCollection Headers { get; } - - /// - /// Whether HTTP pipelining is enabled. The default is true - /// - bool HttpPipeliningEnabled { get; } - - /// - /// KeepAliveInterval - specifies the interval, in milliseconds, between - /// when successive keep-alive packets are sent if no acknowledgement is - /// received. - /// - TimeSpan? KeepAliveInterval { get; } - - /// - /// KeepAliveTime - specifies the timeout, in milliseconds, with no - /// activity until the first keep-alive packet is sent. - /// - TimeSpan? KeepAliveTime { get; } - - /// - /// The maximum amount of time a node is allowed to marked dead - /// - TimeSpan? MaxDeadTimeout { get; } - - /// - /// When a retryable exception occurs or status code is returned this controls the maximum - /// amount of times we should retry the call to Elasticsearch - /// - int? MaxRetries { get; } - - /// - /// Limits the total runtime including retries separately from - ///
-		/// When not specified defaults to  which itself defaults to 60 seconds
-		/// 
- ///
- TimeSpan? MaxRetryTimeout { get; } - - /// Provides a memory stream factory - IMemoryStreamFactory MemoryStreamFactory { get; } - - /// - /// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and - /// always execute on all nodes. - /// When using an implementation that supports reseeding of nodes, this will default to omitting master only - /// node from regular API calls. - /// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken - /// verbatim. - /// - Func NodePredicate { get; } - - /// - /// Allows you to register a callback every time a an API call is returned - /// - Action OnRequestCompleted { get; } - - /// - /// An action to run when the for a request has been - /// created. - /// - Action OnRequestDataCreated { get; } - - /// - /// When enabled, all headers from the HTTP response will be included in the . - /// - bool? ParseAllHeaders { get; } - - /// - /// The timeout in milliseconds to use for ping requests, which are issued to determine whether a node is alive - /// - TimeSpan? PingTimeout { get; } - - /// - /// When set will force all connections through this proxy - /// - string ProxyAddress { get; } - - /// - /// The password for the proxy, when configured - /// - string ProxyPassword { get; } - - /// - /// The username for the proxy, when configured - /// - string ProxyUsername { get; } - - /// - /// Append these query string parameters automatically to every request - /// - NameValueCollection QueryStringParameters { get; } - - /// The serializer to use to serialize requests and deserialize responses - Serializer RequestResponseSerializer { get; } - - /// - /// The timeout in milliseconds for each request to Elasticsearch - /// - TimeSpan RequestTimeout { get; } - - /// - /// A containing the names of all HTTP response headers to attempt to parse and - /// included on the . - /// - HeadersList ResponseHeadersToParse { get; } - - /// - /// Register a ServerCertificateValidationCallback per request - /// - Func ServerCertificateValidationCallback { get; } - - /// - /// During development, the server certificate fingerprint may be provided. When present, it is used to validate the - /// certificate sent by the server. The fingerprint is expected to be the hex string representing the SHA256 public key fingerprint. - /// - string CertificateFingerprint { get; } - - /// - /// Configure the client to skip deserialization of certain status codes e.g: you run Elasticsearch behind a proxy that returns an unexpected - /// json format - /// - IReadOnlyCollection SkipDeserializationForStatusCodes { get; } - - /// - /// Force a new sniff for the cluster when the cluster state information is older than - /// the specified timespan - /// - TimeSpan? SniffInformationLifeSpan { get; } - - /// - /// Force a new sniff for the cluster state every time a connection dies - /// - bool SniffsOnConnectionFault { get; } - - /// - /// Sniff the cluster state immediately on startup - /// - bool SniffsOnStartup { get; } - - /// - /// Instead of following a c/go like error checking on response.IsValid do throw an exception (except when is false) - /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. - /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... - /// - bool ThrowExceptions { get; } - - /// - /// Access to instance that is aware of this instance - /// - UrlFormatter UrlFormatter { get; } - - /// - /// The user agent string to send with requests. Useful for debugging purposes to understand client and framework - /// versions that initiate requests to Elasticsearch - /// - UserAgent UserAgent { get; } - - /// - /// Allow you to override the status code inspection that sets - /// - /// Defaults to validating the statusCode is greater or equal to 200 and less then 300 - /// - /// - /// When the request is using 404 is valid out of the box as well - /// - /// - /// NOTE: if a request specifies this takes precedence - /// - Func StatusCodeToResponseSuccess { get; } - - /// - /// Whether the request should be sent with chunked Transfer-Encoding. - /// - bool TransferEncodingChunked { get; } - - /// - /// DnsRefreshTimeout for the connections. Defaults to 5 minutes. - /// - TimeSpan DnsRefreshTimeout { get; } - - /// - /// Enable statistics about TCP connections to be collected when making a request - /// - bool EnableTcpStats { get; } - - /// - /// Enable statistics about thread pools to be collected when making a request - /// - bool EnableThreadPoolStats { get; } - - /// - /// Provide hints to serializer and products to produce pretty, non minified json. - /// Note: this is not a guarantee you will always get prettified json - /// - bool PrettyJson { get; } - - /// - /// Produces the client meta header for a request. - /// - MetaHeaderProvider MetaHeaderProvider { get; } - - /// - /// Disables the meta header which is included on all requests by default. This header contains lightweight information - /// about the client and runtime. - /// - bool DisableMetaHeader { get; } - } + /// Register a ServerCertificateValidationCallback per request + /// + Func ServerCertificateValidationCallback { get; } + + /// + /// During development, the server certificate fingerprint may be provided. When present, it is used to validate the + /// certificate sent by the server. The fingerprint is expected to be the hex string representing the SHA256 public key fingerprint. + /// + string CertificateFingerprint { get; } + + /// + /// Configure the client to skip deserialization of certain status codes e.g: you run Elasticsearch behind a proxy that returns an unexpected + /// json format + /// + IReadOnlyCollection SkipDeserializationForStatusCodes { get; } + + /// + /// Force a new sniff for the cluster when the cluster state information is older than + /// the specified timespan + /// + TimeSpan? SniffInformationLifeSpan { get; } + + /// + /// Force a new sniff for the cluster state every time a connection dies + /// + bool SniffsOnConnectionFault { get; } + + /// + /// Sniff the cluster state immediately on startup + /// + bool SniffsOnStartup { get; } + + /// + /// Instead of following a c/go like error checking on response.IsValid do throw an exception (except when is false) + /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. + /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... + /// + bool ThrowExceptions { get; } + + /// + /// Access to instance that is aware of this instance + /// + UrlFormatter UrlFormatter { get; } + + /// + /// The user agent string to send with requests. Useful for debugging purposes to understand client and framework + /// versions that initiate requests to Elasticsearch + /// + UserAgent UserAgent { get; } + + /// + /// Allow you to override the status code inspection that sets + /// + /// Defaults to validating the statusCode is greater or equal to 200 and less then 300 + /// + /// + /// When the request is using 404 is valid out of the box as well + /// + /// + /// NOTE: if a request specifies this takes precedence + /// + Func StatusCodeToResponseSuccess { get; } + + /// + /// Whether the request should be sent with chunked Transfer-Encoding. + /// + bool TransferEncodingChunked { get; } + + /// + /// DnsRefreshTimeout for the connections. Defaults to 5 minutes. + /// + TimeSpan DnsRefreshTimeout { get; } + + /// + /// Enable statistics about TCP connections to be collected when making a request + /// + bool EnableTcpStats { get; } + + /// + /// Enable statistics about thread pools to be collected when making a request + /// + bool EnableThreadPoolStats { get; } + + /// + /// Provide hints to serializer and products to produce pretty, non minified json. + /// Note: this is not a guarantee you will always get prettified json + /// + bool PrettyJson { get; } + + /// + /// Produces the client meta header for a request. + /// + MetaHeaderProvider MetaHeaderProvider { get; } + + /// + /// Disables the meta header which is included on all requests by default. This header contains lightweight information + /// about the client and runtime. + /// + bool DisableMetaHeader { get; } } diff --git a/src/Elastic.Transport/Configuration/RequestConfiguration.cs b/src/Elastic.Transport/Configuration/RequestConfiguration.cs index 8725f51..7f22c61 100644 --- a/src/Elastic.Transport/Configuration/RequestConfiguration.cs +++ b/src/Elastic.Transport/Configuration/RequestConfiguration.cs @@ -8,411 +8,410 @@ using System.Security.Cryptography.X509Certificates; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Allows you to inject per overrides to the current . +/// +public interface IRequestConfiguration { /// - /// Allows you to inject per overrides to the current . + /// Force a different Accept header on the request + /// + string Accept { get; set; } + + /// + /// Treat the following statuses (on top of the 200 range) NOT as error. + /// + IReadOnlyCollection AllowedStatusCodes { get; set; } + + /// + /// Provide an authentication header override for this request + /// + AuthorizationHeader AuthenticationHeader { get; set; } + + /// + /// Use the following client certificates to authenticate this single request + /// + X509CertificateCollection ClientCertificates { get; set; } + + /// + /// Force a different Content-Type header on the request + /// + string ContentType { get; set; } + + /// + /// Whether to buffer the request and response bytes for the call + /// + bool? DisableDirectStreaming { get; set; } + + /// + /// Under no circumstance do a ping before the actual call. If a node was previously dead a small ping with + /// low connect timeout will be tried first in normal circumstances + /// + bool? DisablePing { get; set; } + + /// + /// Forces no sniffing to occur on the request no matter what configuration is in place + /// globally + /// + bool? DisableSniff { get; set; } + + /// + /// Whether or not this request should be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining defaults to true + /// + bool? EnableHttpPipelining { get; set; } + + /// + /// This will force the operation on the specified node, this will bypass any configured connection pool and will no retry. + /// + Uri ForceNode { get; set; } + + /// + /// This will override whatever is set on the connection configuration or whatever default the connectionpool has. + /// + int? MaxRetries { get; set; } + + /// + /// Associate an Id with this user-initiated task, such that it can be located in the cluster task list. + /// Valid only for Elasticsearch 6.2.0+ /// - public interface IRequestConfiguration + string OpaqueId { get; set; } + + /// + bool? ParseAllHeaders { get; set; } + + /// + /// The ping timeout for this specific request + /// + TimeSpan? PingTimeout { get; set; } + + /// + /// The timeout for this specific request, takes precedence over the global timeout settings + /// + TimeSpan? RequestTimeout { get; set; } + + /// + HeadersList ResponseHeadersToParse { get; set; } + + /// + /// Submit the request on behalf in the context of a different shield user + ///
https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
+	/// 
+ string RunAs { get; set; } + + /// + /// Instead of following a c/go like error checking on response.IsValid do throw an exception (except when is false) + /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. + /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... + /// + bool? ThrowExceptions { get; set; } + + /// + /// Whether the request should be sent with chunked Transfer-Encoding. + /// + bool? TransferEncodingChunked { get; set; } + + /// + /// Try to send these headers for this single request + /// + NameValueCollection Headers { get; set; } + + /// + bool? EnableTcpStats { get; set; } + + /// + bool? EnableThreadPoolStats { get; set; } + + /// + /// Holds additional meta data about the request. + /// + RequestMetaData RequestMetaData { get; set; } +} + +/// +public class RequestConfiguration : IRequestConfiguration +{ + /// + public string Accept { get; set; } + /// + public IReadOnlyCollection AllowedStatusCodes { get; set; } + /// + public AuthorizationHeader AuthenticationHeader { get; set; } + /// + public X509CertificateCollection ClientCertificates { get; set; } + /// + public string ContentType { get; set; } + /// + public bool? DisableDirectStreaming { get; set; } + /// + public bool? DisablePing { get; set; } + /// + public bool? DisableSniff { get; set; } + /// + public bool? EnableHttpPipelining { get; set; } = true; + /// + public Uri ForceNode { get; set; } + /// + public int? MaxRetries { get; set; } + /// + public string OpaqueId { get; set; } + /// + public TimeSpan? PingTimeout { get; set; } + /// + public TimeSpan? RequestTimeout { get; set; } + /// + public string RunAs { get; set; } + /// + public bool? ThrowExceptions { get; set; } + /// + public bool? TransferEncodingChunked { get; set; } + /// + public NameValueCollection Headers { get; set; } + /// + public bool? EnableTcpStats { get; set; } + /// + public bool? EnableThreadPoolStats { get; set; } + /// + public HeadersList ResponseHeadersToParse { get; set; } + /// + public bool? ParseAllHeaders { get; set; } + + /// + public RequestMetaData RequestMetaData { get; set; } +} + +/// +public class RequestConfigurationDescriptor : IRequestConfiguration +{ + /// + public RequestConfigurationDescriptor(IRequestConfiguration config) { - /// - /// Force a different Accept header on the request - /// - string Accept { get; set; } - - /// - /// Treat the following statuses (on top of the 200 range) NOT as error. - /// - IReadOnlyCollection AllowedStatusCodes { get; set; } - - /// - /// Provide an authentication header override for this request - /// - AuthorizationHeader AuthenticationHeader { get; set; } - - /// - /// Use the following client certificates to authenticate this single request - /// - X509CertificateCollection ClientCertificates { get; set; } - - /// - /// Force a different Content-Type header on the request - /// - string ContentType { get; set; } - - /// - /// Whether to buffer the request and response bytes for the call - /// - bool? DisableDirectStreaming { get; set; } - - /// - /// Under no circumstance do a ping before the actual call. If a node was previously dead a small ping with - /// low connect timeout will be tried first in normal circumstances - /// - bool? DisablePing { get; set; } - - /// - /// Forces no sniffing to occur on the request no matter what configuration is in place - /// globally - /// - bool? DisableSniff { get; set; } - - /// - /// Whether or not this request should be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining defaults to true - /// - bool? EnableHttpPipelining { get; set; } - - /// - /// This will force the operation on the specified node, this will bypass any configured connection pool and will no retry. - /// - Uri ForceNode { get; set; } - - /// - /// This will override whatever is set on the connection configuration or whatever default the connectionpool has. - /// - int? MaxRetries { get; set; } - - /// - /// Associate an Id with this user-initiated task, such that it can be located in the cluster task list. - /// Valid only for Elasticsearch 6.2.0+ - /// - string OpaqueId { get; set; } - - /// - bool? ParseAllHeaders { get; set; } - - /// - /// The ping timeout for this specific request - /// - TimeSpan? PingTimeout { get; set; } - - /// - /// The timeout for this specific request, takes precedence over the global timeout settings - /// - TimeSpan? RequestTimeout { get; set; } - - /// - HeadersList ResponseHeadersToParse { get; set; } - - /// - /// Submit the request on behalf in the context of a different shield user - ///
https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
-		/// 
- string RunAs { get; set; } - - /// - /// Instead of following a c/go like error checking on response.IsValid do throw an exception (except when is false) - /// on the client when a call resulted in an exception on either the client or the Elasticsearch server. - /// Reasons for such exceptions could be search parser errors, index missing exceptions, etc... - /// - bool? ThrowExceptions { get; set; } - - /// - /// Whether the request should be sent with chunked Transfer-Encoding. - /// - bool? TransferEncodingChunked { get; set; } - - /// - /// Try to send these headers for this single request - /// - NameValueCollection Headers { get; set; } - - /// - bool? EnableTcpStats { get; set; } - - /// - bool? EnableThreadPoolStats { get; set; } - - /// - /// Holds additional meta data about the request. - /// - RequestMetaData RequestMetaData { get; set; } + Self.RequestTimeout = config?.RequestTimeout; + Self.PingTimeout = config?.PingTimeout; + Self.ContentType = config?.ContentType; + Self.Accept = config?.Accept; + Self.MaxRetries = config?.MaxRetries; + Self.ForceNode = config?.ForceNode; + Self.DisableSniff = config?.DisableSniff; + Self.DisablePing = config?.DisablePing; + Self.DisableDirectStreaming = config?.DisableDirectStreaming; + Self.AllowedStatusCodes = config?.AllowedStatusCodes; + Self.AuthenticationHeader = config?.AuthenticationHeader; + Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true; + Self.RunAs = config?.RunAs; + Self.ClientCertificates = config?.ClientCertificates; + Self.ThrowExceptions = config?.ThrowExceptions; + Self.OpaqueId = config?.OpaqueId; + Self.TransferEncodingChunked = config?.TransferEncodingChunked; + Self.Headers = config?.Headers; + Self.EnableTcpStats = config?.EnableTcpStats; + Self.EnableThreadPoolStats = config?.EnableThreadPoolStats; + Self.ParseAllHeaders = config?.ParseAllHeaders; + + if (config?.ResponseHeadersToParse is not null) + Self.ResponseHeadersToParse = config.ResponseHeadersToParse; } - /// - public class RequestConfiguration : IRequestConfiguration + string IRequestConfiguration.Accept { get; set; } + IReadOnlyCollection IRequestConfiguration.AllowedStatusCodes { get; set; } + AuthorizationHeader IRequestConfiguration.AuthenticationHeader { get; set; } + X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; } + string IRequestConfiguration.ContentType { get; set; } + bool? IRequestConfiguration.DisableDirectStreaming { get; set; } + bool? IRequestConfiguration.DisablePing { get; set; } + bool? IRequestConfiguration.DisableSniff { get; set; } + bool? IRequestConfiguration.EnableHttpPipelining { get; set; } = true; + Uri IRequestConfiguration.ForceNode { get; set; } + int? IRequestConfiguration.MaxRetries { get; set; } + string IRequestConfiguration.OpaqueId { get; set; } + TimeSpan? IRequestConfiguration.PingTimeout { get; set; } + TimeSpan? IRequestConfiguration.RequestTimeout { get; set; } + string IRequestConfiguration.RunAs { get; set; } + private IRequestConfiguration Self => this; + bool? IRequestConfiguration.ThrowExceptions { get; set; } + bool? IRequestConfiguration.TransferEncodingChunked { get; set; } + NameValueCollection IRequestConfiguration.Headers { get; set; } + bool? IRequestConfiguration.EnableTcpStats { get; set; } + bool? IRequestConfiguration.EnableThreadPoolStats { get; set; } + HeadersList IRequestConfiguration.ResponseHeadersToParse { get; set; } + bool? IRequestConfiguration.ParseAllHeaders { get; set; } + RequestMetaData IRequestConfiguration.RequestMetaData { get; set; } + + + /// + public RequestConfigurationDescriptor RunAs(string username) { - /// - public string Accept { get; set; } - /// - public IReadOnlyCollection AllowedStatusCodes { get; set; } - /// - public AuthorizationHeader AuthenticationHeader { get; set; } - /// - public X509CertificateCollection ClientCertificates { get; set; } - /// - public string ContentType { get; set; } - /// - public bool? DisableDirectStreaming { get; set; } - /// - public bool? DisablePing { get; set; } - /// - public bool? DisableSniff { get; set; } - /// - public bool? EnableHttpPipelining { get; set; } = true; - /// - public Uri ForceNode { get; set; } - /// - public int? MaxRetries { get; set; } - /// - public string OpaqueId { get; set; } - /// - public TimeSpan? PingTimeout { get; set; } - /// - public TimeSpan? RequestTimeout { get; set; } - /// - public string RunAs { get; set; } - /// - public bool? ThrowExceptions { get; set; } - /// - public bool? TransferEncodingChunked { get; set; } - /// - public NameValueCollection Headers { get; set; } - /// - public bool? EnableTcpStats { get; set; } - /// - public bool? EnableThreadPoolStats { get; set; } - /// - public HeadersList ResponseHeadersToParse { get; set; } - /// - public bool? ParseAllHeaders { get; set; } - - /// - public RequestMetaData RequestMetaData { get; set; } + Self.RunAs = username; + return this; } - /// - public class RequestConfigurationDescriptor : IRequestConfiguration + /// + public RequestConfigurationDescriptor RequestTimeout(TimeSpan requestTimeout) + { + Self.RequestTimeout = requestTimeout; + return this; + } + + /// + public RequestConfigurationDescriptor OpaqueId(string opaqueId) + { + Self.OpaqueId = opaqueId; + return this; + } + + /// + public RequestConfigurationDescriptor PingTimeout(TimeSpan pingTimeout) + { + Self.PingTimeout = pingTimeout; + return this; + } + + /// + public RequestConfigurationDescriptor ContentType(string contentTypeHeader) + { + Self.ContentType = contentTypeHeader; + return this; + } + + /// + public RequestConfigurationDescriptor Accept(string acceptHeader) + { + Self.Accept = acceptHeader; + return this; + } + + /// + public RequestConfigurationDescriptor AllowedStatusCodes(IEnumerable codes) + { + Self.AllowedStatusCodes = codes?.ToReadOnlyCollection(); + return this; + } + + /// + public RequestConfigurationDescriptor AllowedStatusCodes(params int[] codes) + { + Self.AllowedStatusCodes = codes?.ToReadOnlyCollection(); + return this; + } + + /// + public RequestConfigurationDescriptor DisableSniffing(bool? disable = true) + { + Self.DisableSniff = disable; + return this; + } + + /// + public RequestConfigurationDescriptor DisablePing(bool? disable = true) + { + Self.DisablePing = disable; + return this; + } + + /// + public RequestConfigurationDescriptor ThrowExceptions(bool throwExceptions = true) + { + Self.ThrowExceptions = throwExceptions; + return this; + } + + /// + public RequestConfigurationDescriptor DisableDirectStreaming(bool? disable = true) + { + Self.DisableDirectStreaming = disable; + return this; + } + + /// + public RequestConfigurationDescriptor ForceNode(Uri uri) + { + Self.ForceNode = uri; + return this; + } + + /// + public RequestConfigurationDescriptor MaxRetries(int retry) + { + Self.MaxRetries = retry; + return this; + } + + /// + public RequestConfigurationDescriptor Authentication(AuthorizationHeader authentication) + { + Self.AuthenticationHeader = authentication; + return this; + } + + /// + public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true) + { + Self.EnableHttpPipelining = enable; + return this; + } + + /// + public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates) + { + Self.ClientCertificates = certificates; + return this; + } + + /// + public RequestConfigurationDescriptor ClientCertificate(X509Certificate certificate) => + ClientCertificates(new X509Certificate2Collection { certificate }); + + /// + public RequestConfigurationDescriptor ClientCertificate(string certificatePath) => + ClientCertificates(new X509Certificate2Collection { new X509Certificate(certificatePath) }); + + /// + public RequestConfigurationDescriptor TransferEncodingChunked(bool? transferEncodingChunked = true) + { + Self.TransferEncodingChunked = transferEncodingChunked; + return this; + } + + /// + public RequestConfigurationDescriptor GlobalHeaders(NameValueCollection headers) + { + Self.Headers = headers; + return this; + } + + /// + public RequestConfigurationDescriptor EnableTcpStats(bool? enableTcpStats = true) + { + Self.EnableTcpStats = enableTcpStats; + return this; + } + + /// + public RequestConfigurationDescriptor EnableThreadPoolStats(bool? enableThreadPoolStats = true) + { + Self.EnableThreadPoolStats = enableThreadPoolStats; + return this; + } + + /// + public RequestConfigurationDescriptor ParseAllHeaders(bool? enable = true) + { + Self.ParseAllHeaders = enable; + return this; + } + + /// + public RequestConfigurationDescriptor ResponseHeadersToParse(IEnumerable headers) + { + Self.ResponseHeadersToParse = new HeadersList(headers); + return this; + } + + /// + public RequestConfigurationDescriptor RequestMetaData(RequestMetaData metaData) { - /// - public RequestConfigurationDescriptor(IRequestConfiguration config) - { - Self.RequestTimeout = config?.RequestTimeout; - Self.PingTimeout = config?.PingTimeout; - Self.ContentType = config?.ContentType; - Self.Accept = config?.Accept; - Self.MaxRetries = config?.MaxRetries; - Self.ForceNode = config?.ForceNode; - Self.DisableSniff = config?.DisableSniff; - Self.DisablePing = config?.DisablePing; - Self.DisableDirectStreaming = config?.DisableDirectStreaming; - Self.AllowedStatusCodes = config?.AllowedStatusCodes; - Self.AuthenticationHeader = config?.AuthenticationHeader; - Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true; - Self.RunAs = config?.RunAs; - Self.ClientCertificates = config?.ClientCertificates; - Self.ThrowExceptions = config?.ThrowExceptions; - Self.OpaqueId = config?.OpaqueId; - Self.TransferEncodingChunked = config?.TransferEncodingChunked; - Self.Headers = config?.Headers; - Self.EnableTcpStats = config?.EnableTcpStats; - Self.EnableThreadPoolStats = config?.EnableThreadPoolStats; - Self.ParseAllHeaders = config?.ParseAllHeaders; - - if (config?.ResponseHeadersToParse is not null) - Self.ResponseHeadersToParse = config.ResponseHeadersToParse; - } - - string IRequestConfiguration.Accept { get; set; } - IReadOnlyCollection IRequestConfiguration.AllowedStatusCodes { get; set; } - AuthorizationHeader IRequestConfiguration.AuthenticationHeader { get; set; } - X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; } - string IRequestConfiguration.ContentType { get; set; } - bool? IRequestConfiguration.DisableDirectStreaming { get; set; } - bool? IRequestConfiguration.DisablePing { get; set; } - bool? IRequestConfiguration.DisableSniff { get; set; } - bool? IRequestConfiguration.EnableHttpPipelining { get; set; } = true; - Uri IRequestConfiguration.ForceNode { get; set; } - int? IRequestConfiguration.MaxRetries { get; set; } - string IRequestConfiguration.OpaqueId { get; set; } - TimeSpan? IRequestConfiguration.PingTimeout { get; set; } - TimeSpan? IRequestConfiguration.RequestTimeout { get; set; } - string IRequestConfiguration.RunAs { get; set; } - private IRequestConfiguration Self => this; - bool? IRequestConfiguration.ThrowExceptions { get; set; } - bool? IRequestConfiguration.TransferEncodingChunked { get; set; } - NameValueCollection IRequestConfiguration.Headers { get; set; } - bool? IRequestConfiguration.EnableTcpStats { get; set; } - bool? IRequestConfiguration.EnableThreadPoolStats { get; set; } - HeadersList IRequestConfiguration.ResponseHeadersToParse { get; set; } - bool? IRequestConfiguration.ParseAllHeaders { get; set; } - RequestMetaData IRequestConfiguration.RequestMetaData { get; set; } - - - /// - public RequestConfigurationDescriptor RunAs(string username) - { - Self.RunAs = username; - return this; - } - - /// - public RequestConfigurationDescriptor RequestTimeout(TimeSpan requestTimeout) - { - Self.RequestTimeout = requestTimeout; - return this; - } - - /// - public RequestConfigurationDescriptor OpaqueId(string opaqueId) - { - Self.OpaqueId = opaqueId; - return this; - } - - /// - public RequestConfigurationDescriptor PingTimeout(TimeSpan pingTimeout) - { - Self.PingTimeout = pingTimeout; - return this; - } - - /// - public RequestConfigurationDescriptor ContentType(string contentTypeHeader) - { - Self.ContentType = contentTypeHeader; - return this; - } - - /// - public RequestConfigurationDescriptor Accept(string acceptHeader) - { - Self.Accept = acceptHeader; - return this; - } - - /// - public RequestConfigurationDescriptor AllowedStatusCodes(IEnumerable codes) - { - Self.AllowedStatusCodes = codes?.ToReadOnlyCollection(); - return this; - } - - /// - public RequestConfigurationDescriptor AllowedStatusCodes(params int[] codes) - { - Self.AllowedStatusCodes = codes?.ToReadOnlyCollection(); - return this; - } - - /// - public RequestConfigurationDescriptor DisableSniffing(bool? disable = true) - { - Self.DisableSniff = disable; - return this; - } - - /// - public RequestConfigurationDescriptor DisablePing(bool? disable = true) - { - Self.DisablePing = disable; - return this; - } - - /// - public RequestConfigurationDescriptor ThrowExceptions(bool throwExceptions = true) - { - Self.ThrowExceptions = throwExceptions; - return this; - } - - /// - public RequestConfigurationDescriptor DisableDirectStreaming(bool? disable = true) - { - Self.DisableDirectStreaming = disable; - return this; - } - - /// - public RequestConfigurationDescriptor ForceNode(Uri uri) - { - Self.ForceNode = uri; - return this; - } - - /// - public RequestConfigurationDescriptor MaxRetries(int retry) - { - Self.MaxRetries = retry; - return this; - } - - /// - public RequestConfigurationDescriptor Authentication(AuthorizationHeader authentication) - { - Self.AuthenticationHeader = authentication; - return this; - } - - /// - public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true) - { - Self.EnableHttpPipelining = enable; - return this; - } - - /// - public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates) - { - Self.ClientCertificates = certificates; - return this; - } - - /// - public RequestConfigurationDescriptor ClientCertificate(X509Certificate certificate) => - ClientCertificates(new X509Certificate2Collection { certificate }); - - /// - public RequestConfigurationDescriptor ClientCertificate(string certificatePath) => - ClientCertificates(new X509Certificate2Collection { new X509Certificate(certificatePath) }); - - /// - public RequestConfigurationDescriptor TransferEncodingChunked(bool? transferEncodingChunked = true) - { - Self.TransferEncodingChunked = transferEncodingChunked; - return this; - } - - /// - public RequestConfigurationDescriptor GlobalHeaders(NameValueCollection headers) - { - Self.Headers = headers; - return this; - } - - /// - public RequestConfigurationDescriptor EnableTcpStats(bool? enableTcpStats = true) - { - Self.EnableTcpStats = enableTcpStats; - return this; - } - - /// - public RequestConfigurationDescriptor EnableThreadPoolStats(bool? enableThreadPoolStats = true) - { - Self.EnableThreadPoolStats = enableThreadPoolStats; - return this; - } - - /// - public RequestConfigurationDescriptor ParseAllHeaders(bool? enable = true) - { - Self.ParseAllHeaders = enable; - return this; - } - - /// - public RequestConfigurationDescriptor ResponseHeadersToParse(IEnumerable headers) - { - Self.ResponseHeadersToParse = new HeadersList(headers); - return this; - } - - /// - public RequestConfigurationDescriptor RequestMetaData(RequestMetaData metaData) - { - Self.RequestMetaData = metaData; - return this; - } + Self.RequestMetaData = metaData; + return this; } } diff --git a/src/Elastic.Transport/Configuration/Security/ApiKey.cs b/src/Elastic.Transport/Configuration/Security/ApiKey.cs index f068a44..801bfa3 100644 --- a/src/Elastic.Transport/Configuration/Security/ApiKey.cs +++ b/src/Elastic.Transport/Configuration/Security/ApiKey.cs @@ -2,26 +2,25 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Credentials for Api Key Authentication +/// +public sealed class ApiKey : AuthorizationHeader { - /// - /// Credentials for Api Key Authentication - /// - public sealed class ApiKey : AuthorizationHeader - { - private readonly string _apiKey; + private readonly string _apiKey; - /// - public ApiKey(string apiKey) => _apiKey = apiKey; + /// + public ApiKey(string apiKey) => _apiKey = apiKey; - /// - public override string AuthScheme { get; } = "ApiKey"; + /// + public override string AuthScheme { get; } = "ApiKey"; - /// - public override bool TryGetAuthorizationParameters(out string value) - { - value = _apiKey; - return true; - } + /// + public override bool TryGetAuthorizationParameters(out string value) + { + value = _apiKey; + return true; } } diff --git a/src/Elastic.Transport/Configuration/Security/AuthorizationHeader.cs b/src/Elastic.Transport/Configuration/Security/AuthorizationHeader.cs index 83aa591..a93c708 100644 --- a/src/Elastic.Transport/Configuration/Security/AuthorizationHeader.cs +++ b/src/Elastic.Transport/Configuration/Security/AuthorizationHeader.cs @@ -2,21 +2,20 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// The HTTP authorization request header used to provide credentials that authenticate a user agent with a server. +/// +public abstract class AuthorizationHeader { /// - /// The HTTP authorization request header used to provide credentials that authenticate a user agent with a server. + /// The authentication scheme that defines how the credentials are encoded. /// - public abstract class AuthorizationHeader - { - /// - /// The authentication scheme that defines how the credentials are encoded. - /// - public abstract string AuthScheme { get; } + public abstract string AuthScheme { get; } - /// - /// If this instance is valid, returns the authorization parameters to include in the header. - /// - public abstract bool TryGetAuthorizationParameters(out string value); - } + /// + /// If this instance is valid, returns the authorization parameters to include in the header. + /// + public abstract bool TryGetAuthorizationParameters(out string value); } diff --git a/src/Elastic.Transport/Configuration/Security/Base64ApiKey.cs b/src/Elastic.Transport/Configuration/Security/Base64ApiKey.cs index 5e44805..84090bf 100644 --- a/src/Elastic.Transport/Configuration/Security/Base64ApiKey.cs +++ b/src/Elastic.Transport/Configuration/Security/Base64ApiKey.cs @@ -5,31 +5,30 @@ using System; using System.Text; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Credentials for Api Key Authentication +/// +public sealed class Base64ApiKey : AuthorizationHeader { - /// - /// Credentials for Api Key Authentication - /// - public sealed class Base64ApiKey : AuthorizationHeader - { - private readonly string _base64String; + private readonly string _base64String; - /// - public Base64ApiKey(string id, string apiKey) => - _base64String = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{apiKey}")); + /// + public Base64ApiKey(string id, string apiKey) => + _base64String = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{apiKey}")); - /// - public Base64ApiKey(string base64EncodedApiKey) => - _base64String = base64EncodedApiKey; + /// + public Base64ApiKey(string base64EncodedApiKey) => + _base64String = base64EncodedApiKey; - /// - public override string AuthScheme { get; } = "ApiKey"; + /// + public override string AuthScheme { get; } = "ApiKey"; - /// - public override bool TryGetAuthorizationParameters(out string value) - { - value = _base64String; - return true; - } + /// + public override bool TryGetAuthorizationParameters(out string value) + { + value = _base64String; + return true; } } diff --git a/src/Elastic.Transport/Configuration/Security/BasicAuthenticationCredentials.cs b/src/Elastic.Transport/Configuration/Security/BasicAuthenticationCredentials.cs index 1c997c3..44d126c 100644 --- a/src/Elastic.Transport/Configuration/Security/BasicAuthenticationCredentials.cs +++ b/src/Elastic.Transport/Configuration/Security/BasicAuthenticationCredentials.cs @@ -5,34 +5,33 @@ using System; using System.Text; -namespace Elastic.Transport -{ - /// - /// Credentials for Basic Authentication. - /// - public sealed class BasicAuthentication : AuthorizationHeader - { - private readonly string _base64String; +namespace Elastic.Transport; - /// The default http header used for basic authentication - public static string BasicAuthenticationScheme { get; } = "Basic"; +/// +/// Credentials for Basic Authentication. +/// +public sealed class BasicAuthentication : AuthorizationHeader +{ + private readonly string _base64String; - /// - public BasicAuthentication(string username, string password) => - _base64String = GetBase64String($"{username}:{password}"); + /// The default http header used for basic authentication + public static string BasicAuthenticationScheme { get; } = "Basic"; - /// - public override string AuthScheme { get; } = BasicAuthenticationScheme; + /// + public BasicAuthentication(string username, string password) => + _base64String = GetBase64String($"{username}:{password}"); - /// - public override bool TryGetAuthorizationParameters(out string value) - { - value = _base64String; - return true; - } + /// + public override string AuthScheme { get; } = BasicAuthenticationScheme; - /// Get Base64 representation for string - public static string GetBase64String(string header) => - Convert.ToBase64String(Encoding.UTF8.GetBytes(header)); + /// + public override bool TryGetAuthorizationParameters(out string value) + { + value = _base64String; + return true; } + + /// Get Base64 representation for string + public static string GetBase64String(string header) => + Convert.ToBase64String(Encoding.UTF8.GetBytes(header)); } diff --git a/src/Elastic.Transport/Configuration/TransportConfiguration.cs b/src/Elastic.Transport/Configuration/TransportConfiguration.cs index b49c793..de3dfa5 100644 --- a/src/Elastic.Transport/Configuration/TransportConfiguration.cs +++ b/src/Elastic.Transport/Configuration/TransportConfiguration.cs @@ -19,464 +19,462 @@ using Elastic.Transport.Extensions; using Elastic.Transport.Products; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Allows you to control how behaves and where/how it connects to Elastic Stack products +/// +public class TransportConfiguration : TransportConfigurationBase { /// - /// Allows you to control how behaves and where/how it connects to Elastic Stack products + /// Detects whether we are running on .NET Core with CurlHandler. + /// If this is true, we will set a very restrictive + /// As the old curl based handler is known to bleed TCP connections: + /// https://github.com/dotnet/runtime/issues/22366 /// - public class TransportConfiguration : TransportConfigurationBase - { - /// - /// Detects whether we are running on .NET Core with CurlHandler. - /// If this is true, we will set a very restrictive - /// As the old curl based handler is known to bleed TCP connections: - /// https://github.com/dotnet/runtime/issues/22366 - /// - private static bool UsingCurlHandler => ConnectionInfo.UsingCurlHandler; - - //public static IMemoryStreamFactory Default { get; } = RecyclableMemoryStreamFactory.Default; - // ReSharper disable once RedundantNameQualifier - /// - /// The default memory stream factory if none is configured on - /// - public static IMemoryStreamFactory DefaultMemoryStreamFactory { get; } = Elastic.Transport.MemoryStreamFactory.Default; - - /// - /// The default ping timeout. Defaults to 2 seconds - /// - public static readonly TimeSpan DefaultPingTimeout = TimeSpan.FromSeconds(2); - - /// - /// The default ping timeout when the connection is over HTTPS. Defaults to - /// 5 seconds - /// - public static readonly TimeSpan DefaultPingTimeoutOnSsl = TimeSpan.FromSeconds(5); - - /// - /// The default timeout before the client aborts a request to Elasticsearch. - /// Defaults to 1 minute - /// - public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); - - /// - /// The default timeout before a TCP connection is forcefully recycled so that DNS updates come through - /// Defaults to 5 minutes. - /// - public static readonly TimeSpan DefaultDnsRefreshTimeout = TimeSpan.FromMinutes(5); + private static bool UsingCurlHandler => ConnectionInfo.UsingCurlHandler; + + //public static MemoryStreamFactory Default { get; } = RecyclableMemoryStreamFactory.Default; + // ReSharper disable once RedundantNameQualifier + /// + /// The default memory stream factory if none is configured on + /// + public static MemoryStreamFactory DefaultMemoryStreamFactory { get; } = Elastic.Transport.DefaultMemoryStreamFactory.Default; + + /// + /// The default ping timeout. Defaults to 2 seconds + /// + public static readonly TimeSpan DefaultPingTimeout = TimeSpan.FromSeconds(2); + + /// + /// The default ping timeout when the connection is over HTTPS. Defaults to + /// 5 seconds + /// + public static readonly TimeSpan DefaultPingTimeoutOnSsl = TimeSpan.FromSeconds(5); + + /// + /// The default timeout before the client aborts a request to Elasticsearch. + /// Defaults to 1 minute + /// + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); + + /// + /// The default timeout before a TCP connection is forcefully recycled so that DNS updates come through + /// Defaults to 5 minutes. + /// + public static readonly TimeSpan DefaultDnsRefreshTimeout = TimeSpan.FromMinutes(5); #pragma warning disable 1587 #pragma warning disable 1570 - /// - /// The default concurrent connection limit for outgoing http requests. Defaults to 80 + /// + /// The default concurrent connection limit for outgoing http requests. Defaults to 80 #if DOTNETCORE - /// Except for implementations based on curl, which defaults to + /// Except for implementations based on curl, which defaults to #endif - /// + /// #pragma warning restore 1570 #pragma warning restore 1587 - public static readonly int DefaultConnectionLimit = UsingCurlHandler ? Environment.ProcessorCount : 80; - - /// - /// Creates a new instance of - /// - /// The root of the Elastic stack product node we want to connect to. Defaults to http://localhost:9200 - /// - public TransportConfiguration(Uri uri = null, IProductRegistration productRegistration = null) - : this(new SingleNodePool(uri ?? new Uri("http://localhost:9200")), productRegistration: productRegistration) { } - - /// - /// Sets up the client to communicate to Elastic Cloud using , - /// documentation for more information on how to obtain your Cloud Id - /// - public TransportConfiguration(string cloudId, BasicAuthentication credentials, IProductRegistration productRegistration = null) - : this(new CloudNodePool(cloudId, credentials), productRegistration: productRegistration) { } - - /// - /// Sets up the client to communicate to Elastic Cloud using , - /// documentation for more information on how to obtain your Cloud Id - /// - public TransportConfiguration(string cloudId, Base64ApiKey credentials, IProductRegistration productRegistration = null) - : this(new CloudNodePool(cloudId, credentials), productRegistration: productRegistration) { } - - /// - /// - /// - /// - /// - public TransportConfiguration( - NodePool nodePool, - ITransportClient connection = null, - Serializer serializer = null, - IProductRegistration productRegistration = null) - : base(nodePool, connection, serializer, productRegistration) { } + public static readonly int DefaultConnectionLimit = UsingCurlHandler ? Environment.ProcessorCount : 80; - } + /// + /// Creates a new instance of + /// + /// The root of the Elastic stack product node we want to connect to. Defaults to http://localhost:9200 + /// + public TransportConfiguration(Uri uri = null, ProductRegistration productRegistration = null) + : this(new SingleNodePool(uri ?? new Uri("http://localhost:9200")), productRegistration: productRegistration) { } + + /// + /// Sets up the client to communicate to Elastic Cloud using , + /// documentation for more information on how to obtain your Cloud Id + /// + public TransportConfiguration(string cloudId, BasicAuthentication credentials, ProductRegistration productRegistration = null) + : this(new CloudNodePool(cloudId, credentials), productRegistration: productRegistration) { } + + /// + /// Sets up the client to communicate to Elastic Cloud using , + /// documentation for more information on how to obtain your Cloud Id + /// + public TransportConfiguration(string cloudId, Base64ApiKey credentials, ProductRegistration productRegistration = null) + : this(new CloudNodePool(cloudId, credentials), productRegistration: productRegistration) { } + + /// + /// + /// + /// + /// + public TransportConfiguration( + NodePool nodePool, + TransportClient connection = null, + Serializer serializer = null, + ProductRegistration productRegistration = null) + : base(nodePool, connection, serializer, productRegistration) { } + +} - /// > - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public abstract class TransportConfigurationBase : ITransportConfiguration - where T : TransportConfigurationBase +/// > +[Browsable(false)] +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class TransportConfigurationBase : ITransportConfiguration + where T : TransportConfigurationBase +{ + private readonly TransportClient _transportClient; + private readonly NodePool _nodePool; + private readonly ProductRegistration _productRegistration; + private readonly NameValueCollection _headers = new NameValueCollection(); + private readonly NameValueCollection _queryString = new NameValueCollection(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly UrlFormatter _urlFormatter; + + private AuthorizationHeader _authenticationHeader; + private X509CertificateCollection _clientCertificates; + private Action _completedRequestHandler = DefaultCompletedRequestHandler; + private int _transportClientLimit; + private TimeSpan? _deadTimeout; + private bool _disableAutomaticProxyDetection; + private bool _disableDirectStreaming; + private bool _disablePings; + private bool _enableHttpCompression; + private bool _enableHttpPipelining = true; + private TimeSpan? _keepAliveInterval; + private TimeSpan? _keepAliveTime; + private TimeSpan? _maxDeadTimeout; + private int? _maxRetries; + private TimeSpan? _maxRetryTimeout; + private Func _nodePredicate; + private Action _onRequestDataCreated = DefaultRequestDataCreated; + private TimeSpan? _pingTimeout; + private string _proxyAddress; + private string _proxyPassword; + private string _proxyUsername; + private TimeSpan _requestTimeout; + private TimeSpan _dnsRefreshTimeout; + private Func _serverCertificateValidationCallback; + private IReadOnlyCollection _skipDeserializationForStatusCodes = new ReadOnlyCollection(new int[] { }); + private TimeSpan? _sniffLifeSpan; + private bool _sniffOnConnectionFault; + private bool _sniffOnStartup; + private bool _throwExceptions; + private bool _transferEncodingChunked; + private MemoryStreamFactory _memoryStreamFactory; + private bool _enableTcpStats; + private bool _enableThreadPoolStats; + private UserAgent _userAgent; + private string _certificateFingerprint; + private bool _disableMetaHeader; + private readonly MetaHeaderProvider _metaHeaderProvider; + + private readonly Func _statusCodeToResponseSuccess; + + /// + /// + /// + /// + /// + /// + /// + protected TransportConfigurationBase(NodePool nodePool, TransportClient transportClient, Serializer requestResponseSerializer, ProductRegistration productRegistration) { - private readonly ITransportClient _transportClient; - private readonly NodePool _nodePool; - private readonly IProductRegistration _productRegistration; - private readonly NameValueCollection _headers = new NameValueCollection(); - private readonly NameValueCollection _queryString = new NameValueCollection(); - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly UrlFormatter _urlFormatter; - - private AuthorizationHeader _authenticationHeader; - private X509CertificateCollection _clientCertificates; - private Action _completedRequestHandler = DefaultCompletedRequestHandler; - private int _transportClientLimit; - private TimeSpan? _deadTimeout; - private bool _disableAutomaticProxyDetection; - private bool _disableDirectStreaming; - private bool _disablePings; - private bool _enableHttpCompression; - private bool _enableHttpPipelining = true; - private TimeSpan? _keepAliveInterval; - private TimeSpan? _keepAliveTime; - private TimeSpan? _maxDeadTimeout; - private int? _maxRetries; - private TimeSpan? _maxRetryTimeout; - private Func _nodePredicate; - private Action _onRequestDataCreated = DefaultRequestDataCreated; - private TimeSpan? _pingTimeout; - private string _proxyAddress; - private string _proxyPassword; - private string _proxyUsername; - private TimeSpan _requestTimeout; - private TimeSpan _dnsRefreshTimeout; - private Func _serverCertificateValidationCallback; - private IReadOnlyCollection _skipDeserializationForStatusCodes = new ReadOnlyCollection(new int[] { }); - private TimeSpan? _sniffLifeSpan; - private bool _sniffOnConnectionFault; - private bool _sniffOnStartup; - private bool _throwExceptions; - private bool _transferEncodingChunked; - private IMemoryStreamFactory _memoryStreamFactory; - private bool _enableTcpStats; - private bool _enableThreadPoolStats; - private UserAgent _userAgent; - private string _certificateFingerprint; - private bool _disableMetaHeader; - private readonly MetaHeaderProvider _metaHeaderProvider; - - private readonly Func _statusCodeToResponseSuccess; - - /// - /// - /// - /// - /// - /// - /// - protected TransportConfigurationBase(NodePool nodePool, ITransportClient transportClient, Serializer requestResponseSerializer, IProductRegistration productRegistration) + _nodePool = nodePool; + _transportClient = transportClient ?? new HttpTransportClient(); + _productRegistration = productRegistration ?? DefaultProductRegistration.Default; + var serializer = requestResponseSerializer ?? new LowLevelRequestResponseSerializer(); + UseThisRequestResponseSerializer = new DiagnosticsSerializerProxy(serializer); + + _transportClientLimit = TransportConfiguration.DefaultConnectionLimit; + _requestTimeout = TransportConfiguration.DefaultTimeout; + _dnsRefreshTimeout = TransportConfiguration.DefaultDnsRefreshTimeout; + _memoryStreamFactory = TransportConfiguration.DefaultMemoryStreamFactory; + _sniffOnConnectionFault = true; + _sniffOnStartup = true; + _sniffLifeSpan = TimeSpan.FromHours(1); + + _metaHeaderProvider = productRegistration?.MetaHeaderProvider; + + _urlFormatter = new UrlFormatter(this); + _statusCodeToResponseSuccess = (m, i) => _productRegistration.HttpStatusCodeClassifier(m, i); + _userAgent = Elastic.Transport.UserAgent.Create(_productRegistration.Name, _productRegistration.GetType()); + + if (nodePool is CloudNodePool cloudPool) { - _nodePool = nodePool; - _transportClient = transportClient ?? new HttpTransportClient(); - _productRegistration = productRegistration ?? ProductRegistration.Default; - var serializer = requestResponseSerializer ?? new LowLevelRequestResponseSerializer(); - UseThisRequestResponseSerializer = new DiagnosticsSerializerProxy(serializer); - - _transportClientLimit = TransportConfiguration.DefaultConnectionLimit; - _requestTimeout = TransportConfiguration.DefaultTimeout; - _dnsRefreshTimeout = TransportConfiguration.DefaultDnsRefreshTimeout; - _memoryStreamFactory = TransportConfiguration.DefaultMemoryStreamFactory; - _sniffOnConnectionFault = true; - _sniffOnStartup = true; - _sniffLifeSpan = TimeSpan.FromHours(1); - - _metaHeaderProvider = productRegistration?.MetaHeaderProvider; - - _urlFormatter = new UrlFormatter(this); - _statusCodeToResponseSuccess = (m, i) => _productRegistration.HttpStatusCodeClassifier(m, i); - _userAgent = Elastic.Transport.UserAgent.Create(_productRegistration.Name, _productRegistration.GetType()); - - if (nodePool is CloudNodePool cloudPool) - { - _authenticationHeader = cloudPool.AuthenticationHeader; - _enableHttpCompression = true; - } - - _headersToParse = new HeadersList(_productRegistration.ResponseHeadersToParse); + _authenticationHeader = cloudPool.AuthenticationHeader; + _enableHttpCompression = true; } - /// - /// Allows more specialized implementations of to use their own - /// request response serializer defaults - /// - // ReSharper disable once MemberCanBePrivate.Global - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global - protected Serializer UseThisRequestResponseSerializer { get; set; } - - AuthorizationHeader ITransportConfiguration.Authentication => _authenticationHeader; - SemaphoreSlim ITransportConfiguration.BootstrapLock => _semaphore; - X509CertificateCollection ITransportConfiguration.ClientCertificates => _clientCertificates; - ITransportClient ITransportConfiguration.Connection => _transportClient; - IProductRegistration ITransportConfiguration.ProductRegistration => _productRegistration; - int ITransportConfiguration.ConnectionLimit => _transportClientLimit; - NodePool ITransportConfiguration.NodePool => _nodePool; - TimeSpan? ITransportConfiguration.DeadTimeout => _deadTimeout; - bool ITransportConfiguration.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; - bool ITransportConfiguration.DisableDirectStreaming => _disableDirectStreaming; - bool ITransportConfiguration.DisablePings => _disablePings; - bool ITransportConfiguration.EnableHttpCompression => _enableHttpCompression; - NameValueCollection ITransportConfiguration.Headers => _headers; - bool ITransportConfiguration.HttpPipeliningEnabled => _enableHttpPipelining; - TimeSpan? ITransportConfiguration.KeepAliveInterval => _keepAliveInterval; - TimeSpan? ITransportConfiguration.KeepAliveTime => _keepAliveTime; - TimeSpan? ITransportConfiguration.MaxDeadTimeout => _maxDeadTimeout; - int? ITransportConfiguration.MaxRetries => _maxRetries; - TimeSpan? ITransportConfiguration.MaxRetryTimeout => _maxRetryTimeout; - IMemoryStreamFactory ITransportConfiguration.MemoryStreamFactory => _memoryStreamFactory; - - Func ITransportConfiguration.NodePredicate => _nodePredicate; - Action ITransportConfiguration.OnRequestCompleted => _completedRequestHandler; - Action ITransportConfiguration.OnRequestDataCreated => _onRequestDataCreated; - TimeSpan? ITransportConfiguration.PingTimeout => _pingTimeout; - string ITransportConfiguration.ProxyAddress => _proxyAddress; - string ITransportConfiguration.ProxyPassword => _proxyPassword; - string ITransportConfiguration.ProxyUsername => _proxyUsername; - NameValueCollection ITransportConfiguration.QueryStringParameters => _queryString; - Serializer ITransportConfiguration.RequestResponseSerializer => UseThisRequestResponseSerializer; - TimeSpan ITransportConfiguration.RequestTimeout => _requestTimeout; - TimeSpan ITransportConfiguration.DnsRefreshTimeout => _dnsRefreshTimeout; - string ITransportConfiguration.CertificateFingerprint => _certificateFingerprint; - - Func ITransportConfiguration.ServerCertificateValidationCallback => - _serverCertificateValidationCallback; - - IReadOnlyCollection ITransportConfiguration.SkipDeserializationForStatusCodes => _skipDeserializationForStatusCodes; - TimeSpan? ITransportConfiguration.SniffInformationLifeSpan => _sniffLifeSpan; - bool ITransportConfiguration.SniffsOnConnectionFault => _sniffOnConnectionFault; - bool ITransportConfiguration.SniffsOnStartup => _sniffOnStartup; - bool ITransportConfiguration.ThrowExceptions => _throwExceptions; - UrlFormatter ITransportConfiguration.UrlFormatter => _urlFormatter; - UserAgent ITransportConfiguration.UserAgent => _userAgent; - Func ITransportConfiguration.StatusCodeToResponseSuccess => _statusCodeToResponseSuccess; - bool ITransportConfiguration.TransferEncodingChunked => _transferEncodingChunked; - bool ITransportConfiguration.EnableTcpStats => _enableTcpStats; - bool ITransportConfiguration.EnableThreadPoolStats => _enableThreadPoolStats; - - void IDisposable.Dispose() => DisposeManagedResources(); - - private static void DefaultCompletedRequestHandler(IApiCallDetails response) { } - - private static void DefaultRequestDataCreated(RequestData response) { } - - /// Assign a private value and return the current - // ReSharper disable once MemberCanBePrivate.Global - protected T Assign(TValue value, Action assigner) => Fluent.Assign((T)this, value, assigner); - - /// - /// Sets the keep-alive option on a TCP connection. - /// For Desktop CLR, sets ServicePointManager.SetTcpKeepAlive - /// - /// - /// - public T EnableTcpKeepAlive(TimeSpan keepAliveTime, TimeSpan keepAliveInterval) => - Assign(keepAliveTime, (a, v) => a._keepAliveTime = v) - .Assign(keepAliveInterval, (a, v) => a._keepAliveInterval = v); - - /// - public T MaximumRetries(int maxRetries) => Assign(maxRetries, (a, v) => a._maxRetries = v); - - /// - /// - /// - /// The connection limit, a value lower then 0 will cause the connection limit not to be set at all - public T ConnectionLimit(int connectionLimit) => Assign(connectionLimit, (a, v) => a._transportClientLimit = v); - - /// - public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => - Assign(sniffsOnConnectionFault, (a, v) => a._sniffOnConnectionFault = v); - - /// - public T SniffOnStartup(bool sniffsOnStartup = true) => Assign(sniffsOnStartup, (a, v) => a._sniffOnStartup = v); - - /// - /// - /// - /// The duration a clusterstate is considered fresh, set to null to disable periodic sniffing - public T SniffLifeSpan(TimeSpan? sniffLifeSpan) => Assign(sniffLifeSpan, (a, v) => a._sniffLifeSpan = v); - - /// - public T EnableHttpCompression(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpCompression = v); - - /// - public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, (a, v) => a._disableAutomaticProxyDetection = v); - - /// - public T ThrowExceptions(bool alwaysThrow = true) => Assign(alwaysThrow, (a, v) => a._throwExceptions = v); - - /// - public T DisablePing(bool disable = true) => Assign(disable, (a, v) => a._disablePings = v); - - /// - // ReSharper disable once MemberCanBePrivate.Global - public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) => Assign(queryStringParameters, (a, v) => a._queryString.Add(v)); - - /// - public T GlobalHeaders(NameValueCollection headers) => Assign(headers, (a, v) => a._headers.Add(v)); - - /// - public T RequestTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._requestTimeout = v); - - /// - public T PingTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._pingTimeout = v); - - /// - public T DeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._deadTimeout = v); - - /// - public T MaxDeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._maxDeadTimeout = v); - - /// - public T MaxRetryTimeout(TimeSpan maxRetryTimeout) => Assign(maxRetryTimeout, (a, v) => a._maxRetryTimeout = v); - - /// - public T DnsRefreshTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._dnsRefreshTimeout = v); - - /// - public T CertificateFingerprint(string fingerprint) => Assign(fingerprint, (a, v) => a._certificateFingerprint = v); - - /// - /// If your connection has to go through proxy, use this method to specify the proxy url - /// - public T Proxy(Uri proxyAddress, string username, string password) => - Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v) - .Assign(username, (a, v) => a._proxyUsername = v) - .Assign(password, (a, v) => a._proxyPassword = v); - - /// - // ReSharper disable once MemberCanBePrivate.Global - public T DisableDirectStreaming(bool b = true) => Assign(b, (a, v) => a._disableDirectStreaming = v); - - /// - public T OnRequestCompleted(Action handler) => - Assign(handler, (a, v) => a._completedRequestHandler += v ?? DefaultCompletedRequestHandler); + _headersToParse = new HeadersList(_productRegistration.ResponseHeadersToParse); + } + + /// + /// Allows more specialized implementations of to use their own + /// request response serializer defaults + /// + // ReSharper disable once MemberCanBePrivate.Global + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global + protected Serializer UseThisRequestResponseSerializer { get; set; } + + AuthorizationHeader ITransportConfiguration.Authentication => _authenticationHeader; + SemaphoreSlim ITransportConfiguration.BootstrapLock => _semaphore; + X509CertificateCollection ITransportConfiguration.ClientCertificates => _clientCertificates; + TransportClient ITransportConfiguration.Connection => _transportClient; + ProductRegistration ITransportConfiguration.ProductRegistration => _productRegistration; + int ITransportConfiguration.ConnectionLimit => _transportClientLimit; + NodePool ITransportConfiguration.NodePool => _nodePool; + TimeSpan? ITransportConfiguration.DeadTimeout => _deadTimeout; + bool ITransportConfiguration.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; + bool ITransportConfiguration.DisableDirectStreaming => _disableDirectStreaming; + bool ITransportConfiguration.DisablePings => _disablePings; + bool ITransportConfiguration.EnableHttpCompression => _enableHttpCompression; + NameValueCollection ITransportConfiguration.Headers => _headers; + bool ITransportConfiguration.HttpPipeliningEnabled => _enableHttpPipelining; + TimeSpan? ITransportConfiguration.KeepAliveInterval => _keepAliveInterval; + TimeSpan? ITransportConfiguration.KeepAliveTime => _keepAliveTime; + TimeSpan? ITransportConfiguration.MaxDeadTimeout => _maxDeadTimeout; + int? ITransportConfiguration.MaxRetries => _maxRetries; + TimeSpan? ITransportConfiguration.MaxRetryTimeout => _maxRetryTimeout; + MemoryStreamFactory ITransportConfiguration.MemoryStreamFactory => _memoryStreamFactory; + + Func ITransportConfiguration.NodePredicate => _nodePredicate; + Action ITransportConfiguration.OnRequestCompleted => _completedRequestHandler; + Action ITransportConfiguration.OnRequestDataCreated => _onRequestDataCreated; + TimeSpan? ITransportConfiguration.PingTimeout => _pingTimeout; + string ITransportConfiguration.ProxyAddress => _proxyAddress; + string ITransportConfiguration.ProxyPassword => _proxyPassword; + string ITransportConfiguration.ProxyUsername => _proxyUsername; + NameValueCollection ITransportConfiguration.QueryStringParameters => _queryString; + Serializer ITransportConfiguration.RequestResponseSerializer => UseThisRequestResponseSerializer; + TimeSpan ITransportConfiguration.RequestTimeout => _requestTimeout; + TimeSpan ITransportConfiguration.DnsRefreshTimeout => _dnsRefreshTimeout; + string ITransportConfiguration.CertificateFingerprint => _certificateFingerprint; + + Func ITransportConfiguration.ServerCertificateValidationCallback => + _serverCertificateValidationCallback; + + IReadOnlyCollection ITransportConfiguration.SkipDeserializationForStatusCodes => _skipDeserializationForStatusCodes; + TimeSpan? ITransportConfiguration.SniffInformationLifeSpan => _sniffLifeSpan; + bool ITransportConfiguration.SniffsOnConnectionFault => _sniffOnConnectionFault; + bool ITransportConfiguration.SniffsOnStartup => _sniffOnStartup; + bool ITransportConfiguration.ThrowExceptions => _throwExceptions; + UrlFormatter ITransportConfiguration.UrlFormatter => _urlFormatter; + UserAgent ITransportConfiguration.UserAgent => _userAgent; + Func ITransportConfiguration.StatusCodeToResponseSuccess => _statusCodeToResponseSuccess; + bool ITransportConfiguration.TransferEncodingChunked => _transferEncodingChunked; + bool ITransportConfiguration.EnableTcpStats => _enableTcpStats; + bool ITransportConfiguration.EnableThreadPoolStats => _enableThreadPoolStats; + + void IDisposable.Dispose() => DisposeManagedResources(); + + private static void DefaultCompletedRequestHandler(ApiCallDetails response) { } + + private static void DefaultRequestDataCreated(RequestData response) { } + + /// Assign a private value and return the current + // ReSharper disable once MemberCanBePrivate.Global + protected T Assign(TValue value, Action assigner) => Fluent.Assign((T)this, value, assigner); + + /// + /// Sets the keep-alive option on a TCP connection. + /// For Desktop CLR, sets ServicePointManager.SetTcpKeepAlive + /// + /// + /// + public T EnableTcpKeepAlive(TimeSpan keepAliveTime, TimeSpan keepAliveInterval) => + Assign(keepAliveTime, (a, v) => a._keepAliveTime = v) + .Assign(keepAliveInterval, (a, v) => a._keepAliveInterval = v); + + /// + public T MaximumRetries(int maxRetries) => Assign(maxRetries, (a, v) => a._maxRetries = v); + + /// + /// + /// + /// The connection limit, a value lower then 0 will cause the connection limit not to be set at all + public T ConnectionLimit(int connectionLimit) => Assign(connectionLimit, (a, v) => a._transportClientLimit = v); - /// - public T OnRequestDataCreated(Action handler) => - Assign(handler, (a, v) => a._onRequestDataCreated += v ?? DefaultRequestDataCreated); + /// + public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => + Assign(sniffsOnConnectionFault, (a, v) => a._sniffOnConnectionFault = v); - /// - public T Authentication(AuthorizationHeader header) => Assign(header, (a, v) => a._authenticationHeader = v); - - /// - public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpPipelining = v); - - /// - /// - /// - /// Return true if you want the node to be used for API calls - public T NodePredicate(Func predicate) => Assign(predicate, (a, v) => a._nodePredicate = v); - - /// - /// Turns on settings that aid in debugging like DisableDirectStreaming() and PrettyJson() - /// so that the original request and response JSON can be inspected. It also always asks the server for the full stack trace on errors - /// - /// - /// An optional callback to be performed when the request completes. This will - /// not overwrite the global OnRequestCompleted callback that is set directly on - /// ConnectionSettings. If no callback is passed, DebugInformation from the response - /// will be written to the debug output by default. - /// - // ReSharper disable once VirtualMemberNeverOverridden.Global - public virtual T EnableDebugMode(Action onRequestCompleted = null) => - PrettyJson() - .DisableDirectStreaming() - .EnableTcpStats() - .EnableThreadPoolStats() - .Assign(onRequestCompleted, (a, v) => - _completedRequestHandler += v ?? (d => Debug.WriteLine(d.DebugInformation))); + /// + public T SniffOnStartup(bool sniffsOnStartup = true) => Assign(sniffsOnStartup, (a, v) => a._sniffOnStartup = v); - private bool _prettyJson; - bool ITransportConfiguration.PrettyJson => _prettyJson; + /// + /// + /// + /// The duration a clusterstate is considered fresh, set to null to disable periodic sniffing + public T SniffLifeSpan(TimeSpan? sniffLifeSpan) => Assign(sniffLifeSpan, (a, v) => a._sniffLifeSpan = v); - /// - // ReSharper disable once VirtualMemberNeverOverridden.Global - // ReSharper disable once MemberCanBeProtected.Global - public virtual T PrettyJson(bool b = true) => Assign(b, (a, v) => a._prettyJson = v); + /// + public T EnableHttpCompression(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpCompression = v); - private bool? _parseAllHeaders; - bool? ITransportConfiguration.ParseAllHeaders => _parseAllHeaders; + /// + public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, (a, v) => a._disableAutomaticProxyDetection = v); - /// - public virtual T ParseAllHeaders(bool b = true) => Assign(b, (a, v) => a._parseAllHeaders = v); + /// + public T ThrowExceptions(bool alwaysThrow = true) => Assign(alwaysThrow, (a, v) => a._throwExceptions = v); - private HeadersList _headersToParse; - HeadersList ITransportConfiguration.ResponseHeadersToParse => _headersToParse; + /// + public T DisablePing(bool disable = true) => Assign(disable, (a, v) => a._disablePings = v); - MetaHeaderProvider ITransportConfiguration.MetaHeaderProvider => _metaHeaderProvider; - - bool ITransportConfiguration.DisableMetaHeader => _disableMetaHeader; + /// + // ReSharper disable once MemberCanBePrivate.Global + public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) => Assign(queryStringParameters, (a, v) => a._queryString.Add(v)); - /// - public virtual T ResponseHeadersToParse(HeadersList headersToParse) - { - _headersToParse = new HeadersList(_headersToParse, headersToParse); - return (T)this; - } + /// + public T GlobalHeaders(NameValueCollection headers) => Assign(headers, (a, v) => a._headers.Add(v)); - /// - public T ServerCertificateValidationCallback(Func callback) => - Assign(callback, (a, v) => a._serverCertificateValidationCallback = v); + /// + public T RequestTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._requestTimeout = v); - /// - public T ClientCertificates(X509CertificateCollection certificates) => - Assign(certificates, (a, v) => a._clientCertificates = v); + /// + public T PingTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._pingTimeout = v); - /// - public T ClientCertificate(X509Certificate certificate) => - Assign(new X509Certificate2Collection { certificate }, (a, v) => a._clientCertificates = v); + /// + public T DeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._deadTimeout = v); - /// - public T ClientCertificate(string certificatePath) => - Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, (a, v) => a._clientCertificates = v); + /// + public T MaxDeadTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._maxDeadTimeout = v); - /// - public T SkipDeserializationForStatusCodes(params int[] statusCodes) => - Assign(new ReadOnlyCollection(statusCodes), (a, v) => a._skipDeserializationForStatusCodes = v); + /// + public T MaxRetryTimeout(TimeSpan maxRetryTimeout) => Assign(maxRetryTimeout, (a, v) => a._maxRetryTimeout = v); - /// - public T UserAgent(UserAgent userAgent) => Assign(userAgent, (a, v) => a._userAgent = v); + /// + public T DnsRefreshTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => a._dnsRefreshTimeout = v); - /// - public T TransferEncodingChunked(bool transferEncodingChunked = true) => Assign(transferEncodingChunked, (a, v) => a._transferEncodingChunked = v); + /// + public T CertificateFingerprint(string fingerprint) => Assign(fingerprint, (a, v) => a._certificateFingerprint = v); - /// - public T MemoryStreamFactory(IMemoryStreamFactory memoryStreamFactory) => Assign(memoryStreamFactory, (a, v) => a._memoryStreamFactory = v); + /// + /// If your connection has to go through proxy, use this method to specify the proxy url + /// + public T Proxy(Uri proxyAddress, string username, string password) => + Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v) + .Assign(username, (a, v) => a._proxyUsername = v) + .Assign(password, (a, v) => a._proxyPassword = v); - /// > - public T EnableTcpStats(bool enableTcpStats = true) => Assign(enableTcpStats, (a, v) => a._enableTcpStats = v); + /// + // ReSharper disable once MemberCanBePrivate.Global + public T DisableDirectStreaming(bool b = true) => Assign(b, (a, v) => a._disableDirectStreaming = v); - /// > - public T EnableThreadPoolStats(bool enableThreadPoolStats = true) => Assign(enableThreadPoolStats, (a, v) => a._enableThreadPoolStats = v); + /// + public T OnRequestCompleted(Action handler) => + Assign(handler, (a, v) => a._completedRequestHandler += v ?? DefaultCompletedRequestHandler); - /// > - public T DisableMetaHeader(bool disable = true) => Assign(disable, (a, v) => a._disableMetaHeader = v); + /// + public T OnRequestDataCreated(Action handler) => + Assign(handler, (a, v) => a._onRequestDataCreated += v ?? DefaultRequestDataCreated); - // ReSharper disable once VirtualMemberNeverOverridden.Global - /// Allows subclasses to hook into the parents dispose - protected virtual void DisposeManagedResources() - { - _nodePool?.Dispose(); - _transportClient?.Dispose(); - _semaphore?.Dispose(); - } + /// + public T Authentication(AuthorizationHeader header) => Assign(header, (a, v) => a._authenticationHeader = v); - /// Allows subclasses to add/remove default global query string parameters - protected T UpdateGlobalQueryString(string key, string value, bool enabled) - { - if (!enabled && _queryString[key] != null) _queryString.Remove(key); - else if (enabled && _queryString[key] == null) - return GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); - return (T)this; - } + /// + public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, (a, v) => a._enableHttpPipelining = v); + /// + /// + /// + /// Return true if you want the node to be used for API calls + public T NodePredicate(Func predicate) => Assign(predicate, (a, v) => a._nodePredicate = v); + + /// + /// Turns on settings that aid in debugging like DisableDirectStreaming() and PrettyJson() + /// so that the original request and response JSON can be inspected. It also always asks the server for the full stack trace on errors + /// + /// + /// An optional callback to be performed when the request completes. This will + /// not overwrite the global OnRequestCompleted callback that is set directly on + /// ConnectionSettings. If no callback is passed, DebugInformation from the response + /// will be written to the debug output by default. + /// + // ReSharper disable once VirtualMemberNeverOverridden.Global + public virtual T EnableDebugMode(Action onRequestCompleted = null) => + PrettyJson() + .DisableDirectStreaming() + .EnableTcpStats() + .EnableThreadPoolStats() + .Assign(onRequestCompleted, (a, v) => + _completedRequestHandler += v ?? (d => Debug.WriteLine(d.DebugInformation))); + + private bool _prettyJson; + bool ITransportConfiguration.PrettyJson => _prettyJson; + + /// + // ReSharper disable once VirtualMemberNeverOverridden.Global + // ReSharper disable once MemberCanBeProtected.Global + public virtual T PrettyJson(bool b = true) => Assign(b, (a, v) => a._prettyJson = v); + + private bool? _parseAllHeaders; + bool? ITransportConfiguration.ParseAllHeaders => _parseAllHeaders; + + /// + public virtual T ParseAllHeaders(bool b = true) => Assign(b, (a, v) => a._parseAllHeaders = v); + + private HeadersList _headersToParse; + HeadersList ITransportConfiguration.ResponseHeadersToParse => _headersToParse; + + MetaHeaderProvider ITransportConfiguration.MetaHeaderProvider => _metaHeaderProvider; + + bool ITransportConfiguration.DisableMetaHeader => _disableMetaHeader; + + /// + public virtual T ResponseHeadersToParse(HeadersList headersToParse) + { + _headersToParse = new HeadersList(_headersToParse, headersToParse); + return (T)this; + } + + /// + public T ServerCertificateValidationCallback(Func callback) => + Assign(callback, (a, v) => a._serverCertificateValidationCallback = v); + + /// + public T ClientCertificates(X509CertificateCollection certificates) => + Assign(certificates, (a, v) => a._clientCertificates = v); + + /// + public T ClientCertificate(X509Certificate certificate) => + Assign(new X509Certificate2Collection { certificate }, (a, v) => a._clientCertificates = v); + + /// + public T ClientCertificate(string certificatePath) => + Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, (a, v) => a._clientCertificates = v); + + /// + public T SkipDeserializationForStatusCodes(params int[] statusCodes) => + Assign(new ReadOnlyCollection(statusCodes), (a, v) => a._skipDeserializationForStatusCodes = v); + + /// + public T UserAgent(UserAgent userAgent) => Assign(userAgent, (a, v) => a._userAgent = v); + + /// + public T TransferEncodingChunked(bool transferEncodingChunked = true) => Assign(transferEncodingChunked, (a, v) => a._transferEncodingChunked = v); + + /// + public T MemoryStreamFactory(MemoryStreamFactory memoryStreamFactory) => Assign(memoryStreamFactory, (a, v) => a._memoryStreamFactory = v); + + /// > + public T EnableTcpStats(bool enableTcpStats = true) => Assign(enableTcpStats, (a, v) => a._enableTcpStats = v); + + /// > + public T EnableThreadPoolStats(bool enableThreadPoolStats = true) => Assign(enableThreadPoolStats, (a, v) => a._enableThreadPoolStats = v); + + /// > + public T DisableMetaHeader(bool disable = true) => Assign(disable, (a, v) => a._disableMetaHeader = v); + + // ReSharper disable once VirtualMemberNeverOverridden.Global + /// Allows subclasses to hook into the parents dispose + protected virtual void DisposeManagedResources() + { + _nodePool?.Dispose(); + _transportClient?.Dispose(); + _semaphore?.Dispose(); + } + + /// Allows subclasses to add/remove default global query string parameters + protected T UpdateGlobalQueryString(string key, string value, bool enabled) + { + if (!enabled && _queryString[key] != null) _queryString.Remove(key); + else if (enabled && _queryString[key] == null) + return GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); + return (T)this; } } diff --git a/src/Elastic.Transport/Configuration/UserAgent.cs b/src/Elastic.Transport/Configuration/UserAgent.cs index 3b104bd..55c931a 100644 --- a/src/Elastic.Transport/Configuration/UserAgent.cs +++ b/src/Elastic.Transport/Configuration/UserAgent.cs @@ -7,46 +7,45 @@ using System.Runtime.InteropServices; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Represents the user agent string. Two constructors exists, one to aid with constructing elastic clients standard compliant +/// user agents and one free form to allow any custom string to be set. +/// +public sealed class UserAgent { - /// - /// Represents the user agent string. Two constructors exists, one to aid with constructing elastic clients standard compliant - /// user agents and one free form to allow any custom string to be set. - /// - public sealed class UserAgent - { - private readonly string _toString; + private readonly string _toString; - private UserAgent(string reposName, Type typeVersionLookup, string[] metadata = null) - { - var version = typeVersionLookup.Assembly - .GetCustomAttribute() - .InformationalVersion; + private UserAgent(string reposName, Type typeVersionLookup, string[] metadata = null) + { + var version = typeVersionLookup.Assembly + .GetCustomAttribute() + .InformationalVersion; - var meta = string.Join("; ", metadata ?? Array.Empty()); - var assemblyName = typeVersionLookup.Assembly.GetName().Name; + var meta = string.Join("; ", metadata ?? Array.Empty()); + var assemblyName = typeVersionLookup.Assembly.GetName().Name; - _toString = $"{reposName}/{version} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; {assemblyName}{meta.Trim()})"; - } + _toString = $"{reposName}/{version} ({RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}; {assemblyName}{meta.Trim()})"; + } - private UserAgent(string fullUserAgentString) => _toString = fullUserAgentString; + private UserAgent(string fullUserAgentString) => _toString = fullUserAgentString; - /// Create a user agent that adhers to the minimum information needed to be elastic standard compliant - /// The repos name uniquely identifies the origin of the client - /// - /// Use 's assembly - /// to inject version information into the header - /// - public static UserAgent Create(string reposName, Type typeVersionLookup) => new UserAgent(reposName, typeVersionLookup); + /// Create a user agent that adhers to the minimum information needed to be elastic standard compliant + /// The repos name uniquely identifies the origin of the client + /// + /// Use 's assembly + /// to inject version information into the header + /// + public static UserAgent Create(string reposName, Type typeVersionLookup) => new UserAgent(reposName, typeVersionLookup); - /// - public static UserAgent Create(string reposName, Type typeVersionLookup, string[] metadata) => new UserAgent(reposName, typeVersionLookup, metadata); + /// + public static UserAgent Create(string reposName, Type typeVersionLookup, string[] metadata) => new UserAgent(reposName, typeVersionLookup, metadata); - /// Create a user string that does not confirm to elastic client standards - public static UserAgent Create(string fullUserAgentString) => new UserAgent(fullUserAgentString); + /// Create a user string that does not confirm to elastic client standards + public static UserAgent Create(string fullUserAgentString) => new UserAgent(fullUserAgentString); - /// The precalculated string representation of this instance - /// - public override string ToString() => _toString; - } + /// The precalculated string representation of this instance + /// + public override string ToString() => _toString; } diff --git a/src/Elastic.Transport/DefaultHttpTransport.cs b/src/Elastic.Transport/DefaultHttpTransport.cs new file mode 100644 index 0000000..fa4df76 --- /dev/null +++ b/src/Elastic.Transport/DefaultHttpTransport.cs @@ -0,0 +1,420 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elastic.Transport.Extensions; +using Elastic.Transport.Products; + +#if !DOTNETCORE +using System.Net; +#endif + +namespace Elastic.Transport; + +/// +public sealed class DefaultHttpTransport : DefaultHttpTransport +{ + /// + /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on + /// different + /// nodes + /// + /// The connection settings to use for this transport + public DefaultHttpTransport(TransportConfiguration configurationValues) : base(configurationValues) + { + } + + /// + /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on + /// different + /// nodes + /// + /// The connection settings to use for this transport + /// The date time proved to use, safe to pass null to use the default + /// The memory stream provider to use, safe to pass null to use the default + public DefaultHttpTransport(TransportConfiguration configurationValues, + DateTimeProvider dateTimeProvider = null, MemoryStreamFactory memoryStreamFactory = null + ) + : base(configurationValues, null, dateTimeProvider, memoryStreamFactory) + { + } + + /// + /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on + /// different + /// nodes + /// + /// The connection settings to use for this transport + /// In charge of create a new pipeline, safe to pass null to use the default + /// The date time proved to use, safe to pass null to use the default + /// The memory stream provider to use, safe to pass null to use the default + internal DefaultHttpTransport(TransportConfiguration configurationValues, + RequestPipelineFactory pipelineProvider = null, + DateTimeProvider dateTimeProvider = null, MemoryStreamFactory memoryStreamFactory = null + ) + : base(configurationValues, pipelineProvider, dateTimeProvider, memoryStreamFactory) + { + } +} + +/// +public class DefaultHttpTransport : HttpTransport + where TConfiguration : class, ITransportConfiguration +{ + private readonly ProductRegistration _productRegistration; + + /// + /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on + /// different + /// nodes + /// + /// The connection settings to use for this transport + public DefaultHttpTransport(TConfiguration configurationValues) : this(configurationValues, null, null, null) + { + } + + /// + /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on + /// different + /// nodes + /// + /// The connection settings to use for this transport + /// The date time proved to use, safe to pass null to use the default + /// The memory stream provider to use, safe to pass null to use the default + public DefaultHttpTransport( + TConfiguration configurationValues, + DateTimeProvider dateTimeProvider = null, + MemoryStreamFactory memoryStreamFactory = null) + : this(configurationValues, null, dateTimeProvider, memoryStreamFactory) { } + + /// + /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on + /// different + /// nodes + /// + /// The connection settings to use for this transport + /// In charge of create a new pipeline, safe to pass null to use the default + /// The date time proved to use, safe to pass null to use the default + /// The memory stream provider to use, safe to pass null to use the default + public DefaultHttpTransport( + TConfiguration configurationValues, + RequestPipelineFactory pipelineProvider = null, + DateTimeProvider dateTimeProvider = null, + MemoryStreamFactory memoryStreamFactory = null + ) + { + configurationValues.ThrowIfNull(nameof(configurationValues)); + configurationValues.NodePool.ThrowIfNull(nameof(configurationValues.NodePool)); + configurationValues.Connection.ThrowIfNull(nameof(configurationValues.Connection)); + configurationValues.RequestResponseSerializer.ThrowIfNull(nameof(configurationValues + .RequestResponseSerializer)); + + _productRegistration = configurationValues.ProductRegistration; + Settings = configurationValues; + PipelineProvider = pipelineProvider ?? new DefaultRequestPipelineFactory(); + DateTimeProvider = dateTimeProvider ?? Elastic.Transport.DefaultDateTimeProvider.Default; + MemoryStreamFactory = memoryStreamFactory ?? configurationValues.MemoryStreamFactory; + } + + private DateTimeProvider DateTimeProvider { get; } + private MemoryStreamFactory MemoryStreamFactory { get; } + private RequestPipelineFactory PipelineProvider { get; } + + /// + /// + /// + public override TConfiguration Settings { get; } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public override TResponse Request(HttpMethod method, string path, PostData data = null, + RequestParameters requestParameters = null) + { + using var pipeline = + PipelineProvider.Create(Settings, DateTimeProvider, MemoryStreamFactory, requestParameters); + + pipeline.FirstPoolUsage(Settings.BootstrapLock); + + var requestData = new RequestData(method, path, data, Settings, requestParameters, MemoryStreamFactory); + Settings.OnRequestDataCreated?.Invoke(requestData); + TResponse response = null; + + var seenExceptions = new List(); + + if (pipeline.TryGetSingleNode(out var singleNode)) + { + // No value in marking a single node as dead. We have no other options! + + requestData.Node = singleNode; + + try + { + response = pipeline.CallProductEndpoint(requestData); + } + catch (PipelineException pipelineException) when (!pipelineException.Recoverable) + { + HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); + } + catch (PipelineException pipelineException) + { + HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); + } + catch (Exception killerException) + { + ThrowUnexpectedTransportException(killerException, seenExceptions, requestData, response, pipeline); + } + } + else + foreach (var node in pipeline.NextNode()) + { + requestData.Node = node; + try + { + if (_productRegistration.SupportsSniff) pipeline.SniffOnStaleCluster(); + if (_productRegistration.SupportsPing) Ping(pipeline, node); + + response = pipeline.CallProductEndpoint(requestData); + if (!response.ApiCallDetails.SuccessOrKnownError) + { + pipeline.MarkDead(node); + if (_productRegistration.SupportsSniff) pipeline.SniffOnConnectionFailure(); + } + } + catch (PipelineException pipelineException) when (!pipelineException.Recoverable) + { + HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); + break; + } + catch (PipelineException pipelineException) + { + HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); + } + catch (Exception killerException) + { + ThrowUnexpectedTransportException(killerException, seenExceptions, requestData, response, + pipeline); + } + + if (response == null || !response.ApiCallDetails.SuccessOrKnownError) continue; // try the next node + + pipeline.MarkAlive(node); + break; + } + + return FinalizeResponse(requestData, pipeline, seenExceptions, response); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public override async Task RequestAsync(HttpMethod method, string path, + PostData data = null, RequestParameters requestParameters = null, + CancellationToken cancellationToken = default) + { + using var pipeline = + PipelineProvider.Create(Settings, DateTimeProvider, MemoryStreamFactory, requestParameters); + + await pipeline.FirstPoolUsageAsync(Settings.BootstrapLock, cancellationToken).ConfigureAwait(false); + + var requestData = new RequestData(method, path, data, Settings, requestParameters, MemoryStreamFactory); + Settings.OnRequestDataCreated?.Invoke(requestData); + TResponse response = null; + + var seenExceptions = new List(); + + if (pipeline.TryGetSingleNode(out var singleNode)) + { + // No value in marking a single node as dead. We have no other options! + + requestData.Node = singleNode; + + try + { + response = await pipeline.CallProductEndpointAsync(requestData, cancellationToken) + .ConfigureAwait(false); + } + catch (PipelineException pipelineException) when (!pipelineException.Recoverable) + { + HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); + } + catch (PipelineException pipelineException) + { + HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); + } + catch (Exception killerException) + { + ThrowUnexpectedTransportException(killerException, seenExceptions, requestData, response, pipeline); + } + } + else + foreach (var node in pipeline.NextNode()) + { + requestData.Node = node; + try + { + if (_productRegistration.SupportsSniff) + await pipeline.SniffOnStaleClusterAsync(cancellationToken).ConfigureAwait(false); + if (_productRegistration.SupportsPing) + await PingAsync(pipeline, node, cancellationToken).ConfigureAwait(false); + + response = await pipeline.CallProductEndpointAsync(requestData, cancellationToken) + .ConfigureAwait(false); + if (!response.ApiCallDetails.SuccessOrKnownError) + { + pipeline.MarkDead(node); + if (_productRegistration.SupportsSniff) + await pipeline.SniffOnConnectionFailureAsync(cancellationToken).ConfigureAwait(false); + } + } + catch (PipelineException pipelineException) when (!pipelineException.Recoverable) + { + HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); + break; + } + catch (PipelineException pipelineException) + { + HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); + } + catch (Exception killerException) + { + if (killerException is OperationCanceledException && cancellationToken.IsCancellationRequested) + pipeline.AuditCancellationRequested(); + + throw new UnexpectedTransportException(killerException, seenExceptions) + { + Request = requestData, ApiCallDetails = response?.ApiCallDetails, AuditTrail = pipeline.AuditTrail + }; + } + + if (cancellationToken.IsCancellationRequested) + { + pipeline.AuditCancellationRequested(); + break; + } + + if (response == null || !response.ApiCallDetails.SuccessOrKnownError) continue; + + pipeline.MarkAlive(node); + break; + } + + return FinalizeResponse(requestData, pipeline, seenExceptions, response); + } + private static void ThrowUnexpectedTransportException(Exception killerException, + List seenExceptions, + RequestData requestData, + TResponse response, RequestPipeline pipeline + ) where TResponse : TransportResponse, new() => + throw new UnexpectedTransportException(killerException, seenExceptions) + { + Request = requestData, ApiCallDetails = response?.ApiCallDetails, AuditTrail = pipeline.AuditTrail + }; + + private static void HandlePipelineException( + ref TResponse response, PipelineException ex, RequestPipeline pipeline, Node node, + ICollection seenExceptions + ) + where TResponse : TransportResponse, new() + { + response ??= ex.Response as TResponse; + pipeline.MarkDead(node); + seenExceptions.Add(ex); + } + + private TResponse FinalizeResponse(RequestData requestData, RequestPipeline pipeline, + List seenExceptions, + TResponse response + ) where TResponse : TransportResponse, new() + { + if (requestData.Node == null) //foreach never ran + pipeline.ThrowNoNodesAttempted(requestData, seenExceptions); + + var callDetails = GetMostRecentCallDetails(response, seenExceptions); + var clientException = pipeline.CreateClientException(response, callDetails, requestData, seenExceptions); + + if (response?.ApiCallDetails == null) + pipeline.BadResponse(ref response, callDetails, requestData, clientException); + + HandleTransportException(requestData, clientException, response); + return response; + } + + private static ApiCallDetails GetMostRecentCallDetails(TResponse response, + IEnumerable seenExceptions) + where TResponse : TransportResponse, new() + { + var callDetails = response?.ApiCallDetails ?? seenExceptions.LastOrDefault(e => e.Response.ApiCallDetails != null)?.Response.ApiCallDetails; + return callDetails; + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private void HandleTransportException(RequestData data, Exception clientException, TransportResponse response) + { + if (response.ApiCallDetails is ApiCallDetails a) + { + //if original exception was not explicitly set during the pipeline + //set it to the TransportException we created for the bad response + if (clientException != null && a.OriginalException == null) + a.OriginalException = clientException; + //On .NET Core the TransportClient implementation throws exceptions on bad responses + //This causes it to behave differently to .NET FULL. We already wrapped the WebException + //under TransportException and it exposes way more information as part of it's + //exception message e.g the the root cause of the server error body. +#if !DOTNETCORE + if (a.OriginalException is WebException) + a.OriginalException = clientException; +#endif + } + + Settings.OnRequestCompleted?.Invoke(response.ApiCallDetails); + if (data != null && clientException != null && data.ThrowExceptions) throw clientException; + } + + private void Ping(RequestPipeline pipeline, Node node) + { + try + { + pipeline.Ping(node); + } + catch (PipelineException e) when (e.Recoverable) + { + if (_productRegistration.SupportsSniff) + pipeline.SniffOnConnectionFailure(); + throw; + } + } + + private async Task PingAsync(RequestPipeline pipeline, Node node, CancellationToken cancellationToken) + { + try + { + await pipeline.PingAsync(node, cancellationToken).ConfigureAwait(false); + } + catch (PipelineException e) when (e.Recoverable) + { + if (_productRegistration.SupportsSniff) + await pipeline.SniffOnConnectionFailureAsync(cancellationToken).ConfigureAwait(false); + throw; + } + } +} diff --git a/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs index 4851c0c..ef252b0 100644 --- a/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs @@ -6,16 +6,15 @@ using System.Collections.Generic; using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// Provides a typed listener to events that emits +public sealed class AuditDiagnosticObserver : TypedDiagnosticObserver { - /// Provides a typed listener to events that emits - public sealed class AuditDiagnosticObserver : TypedDiagnosticObserver - { - /// - public AuditDiagnosticObserver( - Action> onNext, - Action onError = null, - Action onCompleted = null - ) : base(onNext, onError, onCompleted) { } - } + /// + public AuditDiagnosticObserver( + Action> onNext, + Action onError = null, + Action onCompleted = null + ) : base(onNext, onError, onCompleted) { } } diff --git a/src/Elastic.Transport/Diagnostics/Auditing/Audit.cs b/src/Elastic.Transport/Diagnostics/Auditing/Audit.cs index de1915e..b21d641 100644 --- a/src/Elastic.Transport/Diagnostics/Auditing/Audit.cs +++ b/src/Elastic.Transport/Diagnostics/Auditing/Audit.cs @@ -5,56 +5,55 @@ using System; using Elastic.Transport.Extensions; -namespace Elastic.Transport.Diagnostics.Auditing +namespace Elastic.Transport.Diagnostics.Auditing; + +/// An audit of the request made +public sealed class Audit { - /// An audit of the request made - public sealed class Audit + /// > + public Audit(AuditEvent type, DateTime started) { - /// > - public Audit(AuditEvent type, DateTime started) - { - Event = type; - Started = started; - } - - /// - /// The type of audit event - /// - public AuditEvent Event { get; internal set; } - - /// - /// The node on which the request was made - /// - public Node Node { get; internal set; } - - /// - /// The path of the request - /// - public string Path { get; internal set; } - - /// - /// The end date and time of the audit - /// - public DateTime Ended { get; internal set; } - - /// - /// The start date and time of the audit - /// - public DateTime Started { get; } - - /// - /// The exception for the audit, if there was one. - /// - public Exception Exception { get; internal set; } - - /// Returns a string representation of the this audit - public override string ToString() - { - var took = Ended - Started; - var tookString = string.Empty; - if (took >= TimeSpan.Zero) tookString = $" Took: {took}"; - - return Node == null ? $"Event: {Event.GetStringValue()}{tookString}" : $"Event: {Event.GetStringValue()} Node: {Node?.Uri} NodeAlive: {Node?.IsAlive}Took: {tookString}"; - } + Event = type; + Started = started; + } + + /// + /// The type of audit event + /// + public AuditEvent Event { get; internal set; } + + /// + /// The node on which the request was made + /// + public Node Node { get; internal set; } + + /// + /// The path of the request + /// + public string Path { get; internal set; } + + /// + /// The end date and time of the audit + /// + public DateTime Ended { get; internal set; } + + /// + /// The start date and time of the audit + /// + public DateTime Started { get; } + + /// + /// The exception for the audit, if there was one. + /// + public Exception Exception { get; internal set; } + + /// Returns a string representation of the this audit + public override string ToString() + { + var took = Ended - Started; + var tookString = string.Empty; + if (took >= TimeSpan.Zero) tookString = $" Took: {took}"; + + return Node == null ? $"Event: {Event.GetStringValue()}{tookString}" : $"Event: {Event.GetStringValue()} Node: {Node?.Uri} NodeAlive: {Node?.IsAlive}Took: {tookString}"; } } diff --git a/src/Elastic.Transport/Diagnostics/Auditing/AuditEvent.cs b/src/Elastic.Transport/Diagnostics/Auditing/AuditEvent.cs index 63aaf6b..e2716ef 100644 --- a/src/Elastic.Transport/Diagnostics/Auditing/AuditEvent.cs +++ b/src/Elastic.Transport/Diagnostics/Auditing/AuditEvent.cs @@ -2,90 +2,89 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport.Diagnostics.Auditing +namespace Elastic.Transport.Diagnostics.Auditing; + +/// +/// Enumeration of different auditable events that can occur in the execution of +/// as modeled by +/// . +/// +public enum AuditEvent { + /// The request performed the first sniff on startup of the client + SniffOnStartup, + + /// The request saw a failure on a node and a sniff occurred as a result of it + SniffOnFail, + + /// The cluster state expired and a sniff occurred as a result of it + SniffOnStaleCluster, + + /// A sniff that was initiated was successful + SniffSuccess, + + /// A sniff that was initiated resulted in a failure + SniffFailure, + + /// A ping that was initiated was successful + PingSuccess, + + /// A ping that was initiated resulted in a failure + PingFailure, + + /// A node that was previously marked dead was put back in the regular rotation + Resurrection, + + /// + /// All nodes returned by are marked dead (see ) + /// After this event a random node is resurrected and tried by force + /// + AllNodesDead, + + /// + /// A call into resulted in a failure + /// + BadResponse, + + /// + /// A call into resulted in a success + /// + HealthyResponse, + + /// + /// The call into took too long. + /// This could mean the call was retried but retrying was to slow and cumulative this exceeded + /// + /// + MaxTimeoutReached, + + /// + /// The call into was not able to complete + /// successfully and exceeded the available retries as configured on + /// . + /// + MaxRetriesReached, + + /// + /// A call into failed before a response was + /// received. + /// + BadRequest, + + /// + /// Rare but if is too stringent and node nodes in + /// the satisfies this predicate this will result in this failure. + /// + NoNodesAttempted, + + /// + /// Signals the audit may be incomplete because cancellation was requested on the async paths + /// + CancellationRequested, + /// - /// Enumeration of different auditable events that can occur in the execution of - /// as modeled by - /// . + /// The request failed within the allotted but failed + /// on all the available /// - public enum AuditEvent - { - /// The request performed the first sniff on startup of the client - SniffOnStartup, - - /// The request saw a failure on a node and a sniff occurred as a result of it - SniffOnFail, - - /// The cluster state expired and a sniff occurred as a result of it - SniffOnStaleCluster, - - /// A sniff that was initiated was successful - SniffSuccess, - - /// A sniff that was initiated resulted in a failure - SniffFailure, - - /// A ping that was initiated was successful - PingSuccess, - - /// A ping that was initiated resulted in a failure - PingFailure, - - /// A node that was previously marked dead was put back in the regular rotation - Resurrection, - - /// - /// All nodes returned by are marked dead (see ) - /// After this event a random node is resurrected and tried by force - /// - AllNodesDead, - - /// - /// A call into resulted in a failure - /// - BadResponse, - - /// - /// A call into resulted in a success - /// - HealthyResponse, - - /// - /// The call into took too long. - /// This could mean the call was retried but retrying was to slow and cumulative this exceeded - /// - /// - MaxTimeoutReached, - - /// - /// The call into was not able to complete - /// successfully and exceeded the available retries as configured on - /// . - /// - MaxRetriesReached, - - /// - /// A call into failed before a response was - /// received. - /// - BadRequest, - - /// - /// Rare but if is too stringent and node nodes in - /// the satisfies this predicate this will result in this failure. - /// - NoNodesAttempted, - - /// - /// Signals the audit may be incomplete because cancellation was requested on the async paths - /// - CancellationRequested, - - /// - /// The request failed within the allotted but failed - /// on all the available - /// - FailedOverAllNodes, - } + FailedOverAllNodes, } diff --git a/src/Elastic.Transport/Diagnostics/Auditing/AuditEventExtensions.cs b/src/Elastic.Transport/Diagnostics/Auditing/AuditEventExtensions.cs index 4a88220..43bae54 100644 --- a/src/Elastic.Transport/Diagnostics/Auditing/AuditEventExtensions.cs +++ b/src/Elastic.Transport/Diagnostics/Auditing/AuditEventExtensions.cs @@ -5,40 +5,39 @@ using System.Diagnostics; using Elastic.Transport.Extensions; -namespace Elastic.Transport.Diagnostics.Auditing +namespace Elastic.Transport.Diagnostics.Auditing; + +internal static class AuditEventExtensions { - internal static class AuditEventExtensions + /// + /// Returns the name of the event to be used for use in . + /// If this return null the event should not be reported on + /// This indicates this event is monitored by a different component already + /// + /// The diagnostic event name representation or null if it should go unreported + public static string GetAuditDiagnosticEventName(this AuditEvent @event) { - /// - /// Returns the name of the event to be used for use in . - /// If this return null the event should not be reported on - /// This indicates this event is monitored by a different component already - /// - /// The diagnostic event name representation or null if it should go unreported - public static string GetAuditDiagnosticEventName(this AuditEvent @event) + switch(@event) { - switch(@event) - { - case AuditEvent.SniffFailure: - case AuditEvent.SniffSuccess: - case AuditEvent.PingFailure: - case AuditEvent.PingSuccess: - case AuditEvent.BadResponse: - case AuditEvent.HealthyResponse: - return null; - case AuditEvent.SniffOnStartup: return nameof(AuditEvent.SniffOnStartup); - case AuditEvent.SniffOnFail: return nameof(AuditEvent.SniffOnFail); - case AuditEvent.SniffOnStaleCluster: return nameof(AuditEvent.SniffOnStaleCluster); - case AuditEvent.Resurrection: return nameof(AuditEvent.Resurrection); - case AuditEvent.AllNodesDead: return nameof(AuditEvent.AllNodesDead); - case AuditEvent.MaxTimeoutReached: return nameof(AuditEvent.MaxTimeoutReached); - case AuditEvent.MaxRetriesReached: return nameof(AuditEvent.MaxRetriesReached); - case AuditEvent.BadRequest: return nameof(AuditEvent.BadRequest); - case AuditEvent.NoNodesAttempted: return nameof(AuditEvent.NoNodesAttempted); - case AuditEvent.CancellationRequested: return nameof(AuditEvent.CancellationRequested); - case AuditEvent.FailedOverAllNodes: return nameof(AuditEvent.FailedOverAllNodes); - default: return @event.GetStringValue(); //still cached but uses reflection - } + case AuditEvent.SniffFailure: + case AuditEvent.SniffSuccess: + case AuditEvent.PingFailure: + case AuditEvent.PingSuccess: + case AuditEvent.BadResponse: + case AuditEvent.HealthyResponse: + return null; + case AuditEvent.SniffOnStartup: return nameof(AuditEvent.SniffOnStartup); + case AuditEvent.SniffOnFail: return nameof(AuditEvent.SniffOnFail); + case AuditEvent.SniffOnStaleCluster: return nameof(AuditEvent.SniffOnStaleCluster); + case AuditEvent.Resurrection: return nameof(AuditEvent.Resurrection); + case AuditEvent.AllNodesDead: return nameof(AuditEvent.AllNodesDead); + case AuditEvent.MaxTimeoutReached: return nameof(AuditEvent.MaxTimeoutReached); + case AuditEvent.MaxRetriesReached: return nameof(AuditEvent.MaxRetriesReached); + case AuditEvent.BadRequest: return nameof(AuditEvent.BadRequest); + case AuditEvent.NoNodesAttempted: return nameof(AuditEvent.NoNodesAttempted); + case AuditEvent.CancellationRequested: return nameof(AuditEvent.CancellationRequested); + case AuditEvent.FailedOverAllNodes: return nameof(AuditEvent.FailedOverAllNodes); + default: return @event.GetStringValue(); //still cached but uses reflection } } } diff --git a/src/Elastic.Transport/Diagnostics/Auditing/Auditable.cs b/src/Elastic.Transport/Diagnostics/Auditing/Auditable.cs index 45cdcef..3aa7da2 100644 --- a/src/Elastic.Transport/Diagnostics/Auditing/Auditable.cs +++ b/src/Elastic.Transport/Diagnostics/Auditing/Auditable.cs @@ -6,48 +6,47 @@ using System.Collections.Generic; using System.Diagnostics; -namespace Elastic.Transport.Diagnostics.Auditing +namespace Elastic.Transport.Diagnostics.Auditing; + +internal class Auditable : IDisposable { - internal class Auditable : IDisposable + private readonly Audit _audit; + private readonly IDisposable _activity; + private readonly DateTimeProvider _dateTimeProvider; + private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.AuditTrailEvents.SourceName); + + public Auditable(AuditEvent type, List auditTrail, DateTimeProvider dateTimeProvider, Node node) { - private readonly Audit _audit; - private readonly IDisposable _activity; - private readonly IDateTimeProvider _dateTimeProvider; - private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.AuditTrailEvents.SourceName); + _dateTimeProvider = dateTimeProvider; + var started = _dateTimeProvider.Now(); - public Auditable(AuditEvent type, List auditTrail, IDateTimeProvider dateTimeProvider, Node node) + _audit = new Audit(type, started) { - _dateTimeProvider = dateTimeProvider; - var started = _dateTimeProvider.Now(); - - _audit = new Audit(type, started) - { - Node = node - }; - auditTrail.Add(_audit); - var diagnosticName = type.GetAuditDiagnosticEventName(); - _activity = diagnosticName != null ? DiagnosticSource.Diagnose(diagnosticName, _audit) : null; - } - - public AuditEvent Event - { - set => _audit.Event = value; - } + Node = node + }; + auditTrail.Add(_audit); + var diagnosticName = type.GetAuditDiagnosticEventName(); + _activity = diagnosticName != null ? DiagnosticSource.Diagnose(diagnosticName, _audit) : null; + } - public Exception Exception - { - set => _audit.Exception = value; - } + public AuditEvent Event + { + set => _audit.Event = value; + } - public string Path - { - set => _audit.Path = value; - } + public Exception Exception + { + set => _audit.Exception = value; + } - public void Dispose() - { - _audit.Ended = _dateTimeProvider.Now(); - _activity?.Dispose(); - } + public string Path + { + set => _audit.Path = value; + } + + public void Dispose() + { + _audit.Ended = _dateTimeProvider.Now(); + _activity?.Dispose(); } } diff --git a/src/Elastic.Transport/Diagnostics/Diagnostic.cs b/src/Elastic.Transport/Diagnostics/Diagnostic.cs index 58c7921..daeb81c 100644 --- a/src/Elastic.Transport/Diagnostics/Diagnostic.cs +++ b/src/Elastic.Transport/Diagnostics/Diagnostic.cs @@ -5,60 +5,59 @@ using System; using System.Diagnostics; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// +/// Internal subclass of that implements to +/// make it easier to use. +/// +internal class Diagnostic : Diagnostic { - /// - /// Internal subclass of that implements to - /// make it easier to use. - /// - internal class Diagnostic : Diagnostic - { - public Diagnostic(string operationName, DiagnosticSource source, TState state) - : base(operationName, source, state) => - EndState = state; - } + public Diagnostic(string operationName, DiagnosticSource source, TState state) + : base(operationName, source, state) => + EndState = state; +} - internal class Diagnostic : Activity, IDisposable - { - public static Diagnostic Default { get; } = new Diagnostic(); +internal class Diagnostic : Activity, IDisposable +{ + public static Diagnostic Default { get; } = new Diagnostic(); - private readonly DiagnosticSource _source; - private TStateEnd _endState; - private readonly bool _default; - private bool _disposed; + private readonly DiagnosticSource _source; + private TStateEnd _endState; + private readonly bool _default; + private bool _disposed; - private Diagnostic() : base("__NOOP__") => _default = true; + private Diagnostic() : base("__NOOP__") => _default = true; - public Diagnostic(string operationName, DiagnosticSource source, TState state) : base(operationName) - { - _source = source; - _source.StartActivity(SetStartTime(DateTime.UtcNow), state); - } + public Diagnostic(string operationName, DiagnosticSource source, TState state) : base(operationName) + { + _source = source; + _source.StartActivity(SetStartTime(DateTime.UtcNow), state); + } - public TStateEnd EndState + public TStateEnd EndState + { + get => _endState; + internal set { - get => _endState; - internal set - { - //do not store state on default instance - if (_default) return; - _endState = value; - } + //do not store state on default instance + if (_default) return; + _endState = value; } + } - protected override void Dispose(bool disposing) - { - if (_disposed) return; + protected override void Dispose(bool disposing) + { + if (_disposed) return; - if (disposing) - { - //_source can be null if Default instance - _source?.StopActivity(SetEndTime(DateTime.UtcNow), EndState); - } + if (disposing) + { + //_source can be null if Default instance + _source?.StopActivity(SetEndTime(DateTime.UtcNow), EndState); + } - _disposed = true; + _disposed = true; - base.Dispose(disposing); - } + base.Dispose(disposing); } } diff --git a/src/Elastic.Transport/Diagnostics/DiagnosticSources.cs b/src/Elastic.Transport/Diagnostics/DiagnosticSources.cs index b2317b6..3734f61 100644 --- a/src/Elastic.Transport/Diagnostics/DiagnosticSources.cs +++ b/src/Elastic.Transport/Diagnostics/DiagnosticSources.cs @@ -6,136 +6,135 @@ using System.Diagnostics; using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// +/// Provides public access to the strings used while emitting diagnostics. +/// This makes wiring up 's less error prone and eliminates magic strings +/// +public static class DiagnosticSources { /// - /// Provides public access to the strings used while emitting diagnostics. - /// This makes wiring up 's less error prone and eliminates magic strings + /// When subscribing to you will be notified of all decisions in the request pipeline /// - public static class DiagnosticSources - { - /// - /// When subscribing to you will be notified of all decisions in the request pipeline - /// - public static AuditDiagnosticKeys AuditTrailEvents { get; } = new AuditDiagnosticKeys(); + public static AuditDiagnosticKeys AuditTrailEvents { get; } = new AuditDiagnosticKeys(); - /// - /// When subscribing to you will be notified every time a sniff/ping or an API call to Elasticsearch happens - /// - public static RequestPipelineDiagnosticKeys RequestPipeline { get; } = new RequestPipelineDiagnosticKeys(); + /// + /// When subscribing to you will be notified every time a sniff/ping or an API call to Elasticsearch happens + /// + public static RequestPipelineDiagnosticKeys RequestPipeline { get; } = new RequestPipelineDiagnosticKeys(); - /// - /// When subscribing to you will be notified every time a a connection starts and stops a request and starts and stops a a response - /// - public static HttpConnectionDiagnosticKeys HttpConnection { get; } = new HttpConnectionDiagnosticKeys(); + /// + /// When subscribing to you will be notified every time a a connection starts and stops a request and starts and stops a a response + /// + public static HttpConnectionDiagnosticKeys HttpConnection { get; } = new HttpConnectionDiagnosticKeys(); + + /// + /// When subscribing to you will be notified every time a particular serializer writes or reads + /// + public static SerializerDiagnosticKeys Serializer { get; } = new SerializerDiagnosticKeys(); + private interface IDiagnosticsKeys + { + // ReSharper disable once UnusedMemberInSuper.Global /// - /// When subscribing to you will be notified every time a particular serializer writes or reads + /// The source name to enable to receive diagnostic data for this /// - public static SerializerDiagnosticKeys Serializer { get; } = new SerializerDiagnosticKeys(); + string SourceName { get; } + } - private interface IDiagnosticsKeys - { - // ReSharper disable once UnusedMemberInSuper.Global - /// - /// The source name to enable to receive diagnostic data for this - /// - string SourceName { get; } - } + /// + /// Provides access to the string event names related to the default + /// implementation. + /// + public class HttpConnectionDiagnosticKeys : IDiagnosticsKeys + { + /// + public string SourceName { get; } = typeof(HttpTransportClient).FullName; - /// - /// Provides access to the string event names related to the default - /// implementation. - /// - public class HttpConnectionDiagnosticKeys : IDiagnosticsKeys - { - /// - public string SourceName { get; } = typeof(HttpTransportClient).FullName; + /// Start and stop event initiating the request and sending and receiving the headers + public string SendAndReceiveHeaders { get; } = nameof(SendAndReceiveHeaders); - /// Start and stop event initiating the request and sending and receiving the headers - public string SendAndReceiveHeaders { get; } = nameof(SendAndReceiveHeaders); + /// Start and stop event that tracks receiving the body + public string ReceiveBody { get; } = nameof(ReceiveBody); + } - /// Start and stop event that tracks receiving the body - public string ReceiveBody { get; } = nameof(ReceiveBody); - } + /// + /// Provides access to the string event names related to which + /// internally wraps any configured + /// + public class SerializerDiagnosticKeys : IDiagnosticsKeys + { + /// + public string SourceName { get; } = typeof(Serializer).FullName; - /// - /// Provides access to the string event names related to which - /// internally wraps any configured - /// - public class SerializerDiagnosticKeys : IDiagnosticsKeys - { - /// - public string SourceName { get; } = typeof(Serializer).FullName; + /// Start and stop event around invocations + public string Serialize { get; } = nameof(Serialize); - /// Start and stop event around invocations - public string Serialize { get; } = nameof(Serialize); + /// Start and stop event around invocations + public string Deserialize { get; } = nameof(Deserialize); + } - /// Start and stop event around invocations - public string Deserialize { get; } = nameof(Deserialize); - } + /// + /// Provides access to the string event names that emits + /// + public class RequestPipelineDiagnosticKeys : IDiagnosticsKeys + { + /// + public string SourceName { get; } = "RequestPipeline"; /// - /// Provides access to the string event names that emits + /// Start and stop event around invocations /// - public class RequestPipelineDiagnosticKeys : IDiagnosticsKeys - { - /// - public string SourceName { get; } = "RequestPipeline"; + public string CallProductEndpoint { get; } = nameof(CallProductEndpoint); - /// - /// Start and stop event around invocations - /// - public string CallProductEndpoint { get; } = nameof(CallProductEndpoint); + /// Start and stop event around invocations + public string Ping { get; } = nameof(Ping); - /// Start and stop event around invocations - public string Ping { get; } = nameof(Ping); + /// Start and stop event around invocations + public string Sniff { get; } = nameof(Sniff); + } - /// Start and stop event around invocations - public string Sniff { get; } = nameof(Sniff); - } + /// + /// Reference to the diagnostic source name that allows you to listen to all decisions that + /// makes. Events it emits are the names on + /// + public class AuditDiagnosticKeys : IDiagnosticsKeys + { + /// + public string SourceName { get; } = typeof(Audit).FullName; + } - /// - /// Reference to the diagnostic source name that allows you to listen to all decisions that - /// makes. Events it emits are the names on - /// - public class AuditDiagnosticKeys : IDiagnosticsKeys - { - /// - public string SourceName { get; } = typeof(Audit).FullName; - } + internal class EmptyDisposable : IDisposable + { + public void Dispose() { } + } - internal class EmptyDisposable : IDisposable - { - public void Dispose() { } - } + internal static EmptyDisposable SingletonDisposable { get; } = new EmptyDisposable(); - internal static EmptyDisposable SingletonDisposable { get; } = new EmptyDisposable(); + internal static IDisposable Diagnose(this DiagnosticSource source, string operationName, TState state) + { + if (!source.IsEnabled(operationName)) return SingletonDisposable; - internal static IDisposable Diagnose(this DiagnosticSource source, string operationName, TState state) - { - if (!source.IsEnabled(operationName)) return SingletonDisposable; + return new Diagnostic(operationName, source, state); + } - return new Diagnostic(operationName, source, state); - } + internal static Diagnostic Diagnose(this DiagnosticSource source, string operationName, TState state) + { + if (!source.IsEnabled(operationName)) return Diagnostic.Default; - internal static Diagnostic Diagnose(this DiagnosticSource source, string operationName, TState state) - { - if (!source.IsEnabled(operationName)) return Diagnostic.Default; + return new Diagnostic(operationName, source, state); + } - return new Diagnostic(operationName, source, state); - } + internal static Diagnostic Diagnose(this DiagnosticSource source, string operationName, TState state, TEndState endState) + { + if (!source.IsEnabled(operationName)) return Diagnostic.Default; - internal static Diagnostic Diagnose(this DiagnosticSource source, string operationName, TState state, TEndState endState) + return new Diagnostic(operationName, source, state) { - if (!source.IsEnabled(operationName)) return Diagnostic.Default; - - return new Diagnostic(operationName, source, state) - { - EndState = endState - }; - - } + EndState = endState + }; } + } diff --git a/src/Elastic.Transport/Diagnostics/HttpConnectionDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/HttpConnectionDiagnosticObserver.cs index 6e4f02e..178d3d9 100644 --- a/src/Elastic.Transport/Diagnostics/HttpConnectionDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/HttpConnectionDiagnosticObserver.cs @@ -5,18 +5,17 @@ using System; using System.Collections.Generic; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// Provides a typed listener to the events that emits +public sealed class HttpConnectionDiagnosticObserver : TypedDiagnosticObserver { - /// Provides a typed listener to the events that emits - public sealed class HttpConnectionDiagnosticObserver : TypedDiagnosticObserver - { - /// > - public HttpConnectionDiagnosticObserver( - Action> onNextStart, - Action> onNextEnd, - Action onError = null, - Action onCompleted = null - ) : base(onNextStart, onNextEnd, onError, onCompleted) { } + /// > + public HttpConnectionDiagnosticObserver( + Action> onNextStart, + Action> onNextEnd, + Action onError = null, + Action onCompleted = null + ) : base(onNextStart, onNextEnd, onError, onCompleted) { } - } } diff --git a/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs index 4a53f61..1611295 100644 --- a/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs @@ -5,18 +5,17 @@ using System; using System.Collections.Generic; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// Provides a typed listener to actions that takes e.g sniff, ping, or making an API call ; +public sealed class RequestPipelineDiagnosticObserver : TypedDiagnosticObserver { - /// Provides a typed listener to actions that takes e.g sniff, ping, or making an API call ; - public sealed class RequestPipelineDiagnosticObserver : TypedDiagnosticObserver - { - /// - public RequestPipelineDiagnosticObserver( - Action> onNextStart, - Action> onNextEnd, - Action onError = null, - Action onCompleted = null - ) : base(onNextStart, onNextEnd, onError, onCompleted) { } + /// + public RequestPipelineDiagnosticObserver( + Action> onNextStart, + Action> onNextEnd, + Action onError = null, + Action onCompleted = null + ) : base(onNextStart, onNextEnd, onError, onCompleted) { } - } } diff --git a/src/Elastic.Transport/Diagnostics/SerializerDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/SerializerDiagnosticObserver.cs index 1f958ce..142532d 100644 --- a/src/Elastic.Transport/Diagnostics/SerializerDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/SerializerDiagnosticObserver.cs @@ -5,16 +5,15 @@ using System; using System.Collections.Generic; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// Provides a typed listener any time an does a write or read +public sealed class SerializerDiagnosticObserver : TypedDiagnosticObserver { - /// Provides a typed listener any time an does a write or read - public sealed class SerializerDiagnosticObserver : TypedDiagnosticObserver - { - /// - public SerializerDiagnosticObserver( - Action> onNext, - Action onError = null, - Action onCompleted = null - ) : base(onNext, onError, onCompleted) { } - } + /// + public SerializerDiagnosticObserver( + Action> onNext, + Action onError = null, + Action onCompleted = null + ) : base(onNext, onError, onCompleted) { } } diff --git a/src/Elastic.Transport/Diagnostics/TcpStats.cs b/src/Elastic.Transport/Diagnostics/TcpStats.cs index 9aa06ea..47f4c8f 100644 --- a/src/Elastic.Transport/Diagnostics/TcpStats.cs +++ b/src/Elastic.Transport/Diagnostics/TcpStats.cs @@ -7,56 +7,55 @@ using System.Collections.ObjectModel; using System.Net.NetworkInformation; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// +/// Gets statistics about TCP connections +/// +internal static class TcpStats { + private static readonly int StateLength = Enum.GetNames(typeof(TcpState)).Length; + /// - /// Gets statistics about TCP connections + /// Gets the active TCP connections /// - internal static class TcpStats - { - private static readonly int StateLength = Enum.GetNames(typeof(TcpState)).Length; - - /// - /// Gets the active TCP connections - /// - /// - public static TcpConnectionInformation[] GetActiveTcpConnections() => - IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections(); + /// + public static TcpConnectionInformation[] GetActiveTcpConnections() => + IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections(); - /// - /// Gets the sum for each state of the active TCP connections - /// - public static ReadOnlyDictionary GetStates() + /// + /// Gets the sum for each state of the active TCP connections + /// + public static ReadOnlyDictionary GetStates() + { + var states = new Dictionary(StateLength); + var connections = GetActiveTcpConnections(); + for (var index = 0; index < connections.Length; index++) { - var states = new Dictionary(StateLength); - var connections = GetActiveTcpConnections(); - for (var index = 0; index < connections.Length; index++) - { - var connection = connections[index]; - if (states.TryGetValue(connection.State, out var count)) - states[connection.State] = ++count; - else - states.Add(connection.State, 1); - } - - return new ReadOnlyDictionary(states); + var connection = connections[index]; + if (states.TryGetValue(connection.State, out var count)) + states[connection.State] = ++count; + else + states.Add(connection.State, 1); } - /// - /// Gets the TCP statistics for a given network interface component - /// - public static TcpStatistics GetTcpStatistics(NetworkInterfaceComponent version) + return new ReadOnlyDictionary(states); + } + + /// + /// Gets the TCP statistics for a given network interface component + /// + public static TcpStatistics GetTcpStatistics(NetworkInterfaceComponent version) + { + var properties = IPGlobalProperties.GetIPGlobalProperties(); + switch (version) { - var properties = IPGlobalProperties.GetIPGlobalProperties(); - switch (version) - { - case NetworkInterfaceComponent.IPv4: - return properties.GetTcpIPv4Statistics(); - case NetworkInterfaceComponent.IPv6: - return properties.GetTcpIPv6Statistics(); - default: - throw new ArgumentException("version"); - } + case NetworkInterfaceComponent.IPv4: + return properties.GetTcpIPv4Statistics(); + case NetworkInterfaceComponent.IPv6: + return properties.GetTcpIPv6Statistics(); + default: + throw new ArgumentException("version"); } } } diff --git a/src/Elastic.Transport/Diagnostics/ThreadpoolStats.cs b/src/Elastic.Transport/Diagnostics/ThreadpoolStats.cs index 8d3551f..cb30b85 100644 --- a/src/Elastic.Transport/Diagnostics/ThreadpoolStats.cs +++ b/src/Elastic.Transport/Diagnostics/ThreadpoolStats.cs @@ -6,62 +6,61 @@ using System.Collections.ObjectModel; using System.Threading; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// Retrieves statistics for thread pools +public static class ThreadPoolStats { - /// Retrieves statistics for thread pools - public static class ThreadPoolStats - { - private static readonly string WorkerThreads = "Worker"; - private static readonly string CompletionPortThreads = "IOCP"; + private static readonly string WorkerThreads = "Worker"; + private static readonly string CompletionPortThreads = "IOCP"; - /// Retrieve thread pool statistics - public static ReadOnlyDictionary GetStats() - { - var dictionary = new Dictionary(2); - ThreadPool.GetMaxThreads(out var maxWorkerThreads, out var maxIoThreads); - ThreadPool.GetAvailableThreads(out var freeWorkerThreads, out var freeIoThreads); - ThreadPool.GetMinThreads(out var minWorkerThreads, out var minIoThreads); - var busyIoThreads = maxIoThreads - freeIoThreads; - var busyWorkerThreads = maxWorkerThreads - freeWorkerThreads; + /// Retrieve thread pool statistics + public static ReadOnlyDictionary GetStats() + { + var dictionary = new Dictionary(2); + ThreadPool.GetMaxThreads(out var maxWorkerThreads, out var maxIoThreads); + ThreadPool.GetAvailableThreads(out var freeWorkerThreads, out var freeIoThreads); + ThreadPool.GetMinThreads(out var minWorkerThreads, out var minIoThreads); + var busyIoThreads = maxIoThreads - freeIoThreads; + var busyWorkerThreads = maxWorkerThreads - freeWorkerThreads; - dictionary.Add(WorkerThreads, new ThreadPoolStatistics(minWorkerThreads, maxWorkerThreads, busyWorkerThreads, freeWorkerThreads)); - dictionary.Add(CompletionPortThreads, new ThreadPoolStatistics(minIoThreads, maxIoThreads, busyIoThreads, freeIoThreads)); - return new ReadOnlyDictionary(dictionary); - } + dictionary.Add(WorkerThreads, new ThreadPoolStatistics(minWorkerThreads, maxWorkerThreads, busyWorkerThreads, freeWorkerThreads)); + dictionary.Add(CompletionPortThreads, new ThreadPoolStatistics(minIoThreads, maxIoThreads, busyIoThreads, freeIoThreads)); + return new ReadOnlyDictionary(dictionary); } +} - /// Statistics for a thread pool - public class ThreadPoolStatistics +/// Statistics for a thread pool +public class ThreadPoolStatistics +{ + /// > + public ThreadPoolStatistics(int min, int max, int busy, int free) { - /// > - public ThreadPoolStatistics(int min, int max, int busy, int free) - { - Min = min; - Max = max; - Busy = busy; - Free = free; - } + Min = min; + Max = max; + Busy = busy; + Free = free; + } - /// The difference between the maximum number of thread pool threads returned by - /// , and the number currently free. - /// - public int Busy { get; } + /// The difference between the maximum number of thread pool threads returned by + /// , and the number currently free. + /// + public int Busy { get; } - /// The difference between the maximum number of thread pool threads returned by - /// , and the number currently active. - /// - public int Free { get; } + /// The difference between the maximum number of thread pool threads returned by + /// , and the number currently active. + /// + public int Free { get; } - /// - /// The number of requests to the thread pool that can be active concurrently. All requests above that number remain queued until - /// thread pool threads become available. - /// - public int Max { get; } + /// + /// The number of requests to the thread pool that can be active concurrently. All requests above that number remain queued until + /// thread pool threads become available. + /// + public int Max { get; } - /// - /// The minimum number of threads the thread pool creates on demand, as new requests are made, before switching to an algorithm for - /// managing thread creation and destruction. - /// - public int Min { get; } - } + /// + /// The minimum number of threads the thread pool creates on demand, as new requests are made, before switching to an algorithm for + /// managing thread creation and destruction. + /// + public int Min { get; } } diff --git a/src/Elastic.Transport/Diagnostics/TypedDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/TypedDiagnosticObserver.cs index bfb2da1..b7a547e 100644 --- a/src/Elastic.Transport/Diagnostics/TypedDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/TypedDiagnosticObserver.cs @@ -6,80 +6,79 @@ using System.Collections.Generic; using System.Diagnostics; -namespace Elastic.Transport.Diagnostics +namespace Elastic.Transport.Diagnostics; + +/// +/// Provides a base implementation of that makes it easier to consume +/// the 's exposed in this library +/// +public abstract class TypedDiagnosticObserver : IObserver> { - /// - /// Provides a base implementation of that makes it easier to consume - /// the 's exposed in this library - /// - public abstract class TypedDiagnosticObserver : IObserver> + private readonly Action> _onNext; + private readonly Action _onError; + private readonly Action _onCompleted; + + /// + protected TypedDiagnosticObserver( + Action> onNext, + Action onError = null, + Action onCompleted = null + ) { - private readonly Action> _onNext; - private readonly Action _onError; - private readonly Action _onCompleted; - - /// - protected TypedDiagnosticObserver( - Action> onNext, - Action onError = null, - Action onCompleted = null - ) - { - _onNext= onNext; - _onError = onError; - _onCompleted = onCompleted; - } - - void IObserver>.OnCompleted() => _onCompleted?.Invoke(); - - void IObserver>.OnError(Exception error) => _onError?.Invoke(error); - - void IObserver>.OnNext(KeyValuePair value) - { - if (value.Value is TOnNext next) _onNext?.Invoke(new KeyValuePair(value.Key, next)); - else if (value.Value == null) _onNext?.Invoke(new KeyValuePair(value.Key, default)); - - else throw new Exception($"{value.Key} received unexpected type {value.Value.GetType()}"); - - } + _onNext= onNext; + _onError = onError; + _onCompleted = onCompleted; } + void IObserver>.OnCompleted() => _onCompleted?.Invoke(); + + void IObserver>.OnError(Exception error) => _onError?.Invoke(error); + + void IObserver>.OnNext(KeyValuePair value) + { + if (value.Value is TOnNext next) _onNext?.Invoke(new KeyValuePair(value.Key, next)); + else if (value.Value == null) _onNext?.Invoke(new KeyValuePair(value.Key, default)); + + else throw new Exception($"{value.Key} received unexpected type {value.Value.GetType()}"); + + } +} + +/// +public abstract class TypedDiagnosticObserver : IObserver> +{ + private readonly Action> _onNextStart; + private readonly Action> _onNextEnd; + private readonly Action _onError; + private readonly Action _onCompleted; + /// - public abstract class TypedDiagnosticObserver : IObserver> + protected TypedDiagnosticObserver( + Action> onNextStart, + Action> onNextEnd, + Action onError = null, + Action onCompleted = null + ) + { + _onNextStart = onNextStart; + _onNextEnd = onNextEnd; + _onError = onError; + _onCompleted = onCompleted; + } + + void IObserver>.OnCompleted() => _onCompleted?.Invoke(); + + void IObserver>.OnError(Exception error) => _onError?.Invoke(error); + + void IObserver>.OnNext(KeyValuePair value) { - private readonly Action> _onNextStart; - private readonly Action> _onNextEnd; - private readonly Action _onError; - private readonly Action _onCompleted; - - /// - protected TypedDiagnosticObserver( - Action> onNextStart, - Action> onNextEnd, - Action onError = null, - Action onCompleted = null - ) - { - _onNextStart = onNextStart; - _onNextEnd = onNextEnd; - _onError = onError; - _onCompleted = onCompleted; - } - - void IObserver>.OnCompleted() => _onCompleted?.Invoke(); - - void IObserver>.OnError(Exception error) => _onError?.Invoke(error); - - void IObserver>.OnNext(KeyValuePair value) - { - if (value.Value is TOnNextStart nextStart) _onNextStart?.Invoke(new KeyValuePair(value.Key, nextStart)); - else if (value.Key.EndsWith(".Start") && value.Value is null) _onNextStart?.Invoke(new KeyValuePair(value.Key, default)); - - else if (value.Value is TOnNextEnd nextEnd) _onNextEnd?.Invoke(new KeyValuePair(value.Key, nextEnd)); - else if (value.Key.EndsWith(".Stop") && value.Value is null) _onNextEnd?.Invoke(new KeyValuePair(value.Key, default)); - - else throw new Exception($"{value.Key} received unexpected type {value.Value.GetType()}"); - - } + if (value.Value is TOnNextStart nextStart) _onNextStart?.Invoke(new KeyValuePair(value.Key, nextStart)); + else if (value.Key.EndsWith(".Start") && value.Value is null) _onNextStart?.Invoke(new KeyValuePair(value.Key, default)); + + else if (value.Value is TOnNextEnd nextEnd) _onNextEnd?.Invoke(new KeyValuePair(value.Key, nextEnd)); + else if (value.Key.EndsWith(".Stop") && value.Value is null) _onNextEnd?.Invoke(new KeyValuePair(value.Key, default)); + + else throw new Exception($"{value.Key} received unexpected type {value.Value.GetType()}"); + } } diff --git a/src/Elastic.Transport/Elastic.Transport.csproj b/src/Elastic.Transport/Elastic.Transport.csproj index 905c4ad..bfb0c44 100644 --- a/src/Elastic.Transport/Elastic.Transport.csproj +++ b/src/Elastic.Transport/Elastic.Transport.csproj @@ -18,6 +18,11 @@ netstandard2.0;netstandard2.1;net461;net5.0;net6.0 + + + + + diff --git a/src/Elastic.Transport/Exceptions/TransportException.cs b/src/Elastic.Transport/Exceptions/TransportException.cs index b26ad63..c8b1b8e 100644 --- a/src/Elastic.Transport/Exceptions/TransportException.cs +++ b/src/Elastic.Transport/Exceptions/TransportException.cs @@ -8,114 +8,112 @@ using System.Text; using Elastic.Transport.Diagnostics.Auditing; using Elastic.Transport.Extensions; -using static Elastic.Transport.ResponseStatics; +using static Elastic.Transport.Diagnostics.ResponseStatics; -namespace Elastic.Transport -{ +namespace Elastic.Transport; - /// - /// Exceptions that occur are wrapped inside - /// this exception. This is done to not lose valuable diagnostic information. - /// - /// - /// When is set these exceptions are rethrown and need - /// to be caught - /// - /// - public class TransportException : Exception - { - /// - public TransportException(string message) : base(message) => FailureReason = PipelineFailure.Unexpected; +/// +/// Exceptions that occur are wrapped inside +/// this exception. This is done to not lose valuable diagnostic information. +/// +/// +/// When is set these exceptions are rethrown and need +/// to be caught +/// +/// +public class TransportException : Exception +{ + /// + public TransportException(string message) : base(message) => FailureReason = PipelineFailure.Unexpected; - /// - public TransportException(PipelineFailure failure, string message, Exception innerException) - : base(message, innerException) => FailureReason = failure; + /// + public TransportException(PipelineFailure failure, string message, Exception innerException) + : base(message, innerException) => FailureReason = failure; - /// - public TransportException(PipelineFailure failure, string message, IApiCallDetails apiCall) - : this(message) - { - Response = apiCall; - FailureReason = failure; - AuditTrail = apiCall?.AuditTrail; - } + /// + public TransportException(PipelineFailure failure, string message, TransportResponse response) + : this(message) + { + ApiCallDetails = response.ApiCallDetails; + FailureReason = failure; + AuditTrail = response.ApiCallDetails?.AuditTrail; + } - /// - /// The audit trail keeping track of what happened during the invocation of - /// up until the moment of this exception - /// - public IEnumerable AuditTrail { get; internal set; } + /// + /// The audit trail keeping track of what happened during the invocation of + /// up until the moment of this exception + /// + public IEnumerable AuditTrail { get; internal set; } - /// - /// The reason this exception occurred was one of the well defined exit points as modelled by - /// - /// - // ReSharper disable once MemberCanBePrivate.Global - public PipelineFailure? FailureReason { get; } + /// + /// The reason this exception occurred was one of the well defined exit points as modelled by + /// + /// + // ReSharper disable once MemberCanBePrivate.Global + public PipelineFailure? FailureReason { get; } - /// Information about the request that triggered this exception - public RequestData Request { get; internal set; } + /// Information about the request that triggered this exception + public RequestData Request { get; internal set; } - /// The response if available that triggered the exception - public IApiCallDetails Response { get; internal set; } + /// The response if available that triggered the exception + public ApiCallDetails ApiCallDetails { get; internal set; } - /// - /// A self describing human readable string explaining why this exception was thrown. - /// Useful in logging and diagnosing + reporting issues! - /// - // ReSharper disable once UnusedMember.Global - public string DebugInformation + /// + /// A self describing human readable string explaining why this exception was thrown. + /// Useful in logging and diagnosing + reporting issues! + /// + // ReSharper disable once UnusedMember.Global + public string DebugInformation + { + get { - get - { - var sb = new StringBuilder(); - var failureReason = FailureReason.GetStringValue(); - if (FailureReason == PipelineFailure.Unexpected && AuditTrail.HasAny(out var auditTrail)) - failureReason = "Unrecoverable/Unexpected " + auditTrail.Last().Event.GetStringValue(); + var sb = new StringBuilder(); + var failureReason = FailureReason.GetStringValue(); + if (FailureReason == PipelineFailure.Unexpected && AuditTrail.HasAny(out var auditTrail)) + failureReason = "Unrecoverable/Unexpected " + auditTrail.Last().Event.GetStringValue(); - sb.Append("# FailureReason: ") - .Append(failureReason) - .Append(" while attempting "); + sb.Append("# FailureReason: ") + .Append(failureReason) + .Append(" while attempting "); - if (Request != null) - { - sb.Append(Request.Method.GetStringValue()).Append(" on "); - if (Request.Uri != null) - sb.AppendLine(Request.Uri.ToString()); - else - { - sb.Append(Request.PathAndQuery) - .AppendLine(" on an empty node, likely a node predicate on ConnectionSettings not matching ANY nodes"); - } - } - else if (Response != null) - { - sb.Append(Response.HttpMethod.GetStringValue()) - .Append(" on ") - .AppendLine(Response.Uri.ToString()); - } - else - sb.AppendLine("a request"); - - if (Response != null) - DebugInformationBuilder(Response, sb); + if (Request != null) + { + sb.Append(Request.Method.GetStringValue()).Append(" on "); + if (Request.Uri != null) + sb.AppendLine(Request.Uri.ToString()); else { - DebugAuditTrail(AuditTrail, sb); - DebugAuditTrailExceptions(AuditTrail, sb); + sb.Append(Request.PathAndQuery) + .AppendLine(" on an empty node, likely a node predicate on ConnectionSettings not matching ANY nodes"); } + } + else if (ApiCallDetails != null) + { + sb.Append(ApiCallDetails.HttpMethod.GetStringValue()) + .Append(" on ") + .AppendLine(ApiCallDetails.Uri.ToString()); + } + else + sb.AppendLine("a request"); - if (InnerException != null) - { - sb.Append("# Inner Exception: ") - .AppendLine(InnerException.Message) - .AppendLine(InnerException.ToString()); - } + if (ApiCallDetails != null) + DebugInformationBuilder(ApiCallDetails, sb); + else + { + DebugAuditTrail(AuditTrail, sb); + DebugAuditTrailExceptions(AuditTrail, sb); + } - sb.AppendLine("# Exception:") - .AppendLine(ToString()); - return sb.ToString(); + if (InnerException != null) + { + sb.Append("# Inner Exception: ") + .AppendLine(InnerException.Message) + .AppendLine(InnerException.ToString()); } + + sb.AppendLine("# Exception:") + .AppendLine(ToString()); + return sb.ToString(); } } } diff --git a/src/Elastic.Transport/Exceptions/UnexpectedTransportException.cs b/src/Elastic.Transport/Exceptions/UnexpectedTransportException.cs index b6067c1..f6c1c36 100644 --- a/src/Elastic.Transport/Exceptions/UnexpectedTransportException.cs +++ b/src/Elastic.Transport/Exceptions/UnexpectedTransportException.cs @@ -6,26 +6,22 @@ using System.Collections.Generic; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// An exception occured that was not the result of one the well defined exit points as modelled by +/// . This exception will always bubble out. +/// +public class UnexpectedTransportException : TransportException { + /// + public UnexpectedTransportException(Exception killerException, IReadOnlyCollection seenExceptions) + : base(PipelineFailure.Unexpected, killerException?.Message ?? "An unexpected exception occurred.", killerException) => + SeenExceptions = seenExceptions ?? EmptyReadOnly.Collection; /// - /// An exception occured that was not the result of one the well defined exit points as modelled by - /// . This exception will always bubble out. + /// Seen Exceptions that we try to failover on before this was thrown. /// - public class UnexpectedTransportException : TransportException - { - /// - public UnexpectedTransportException(Exception killerException, IReadOnlyCollection seenExceptions) - : base(PipelineFailure.Unexpected, killerException?.Message ?? "An unexpected exception occurred.", killerException) => - SeenExceptions = seenExceptions ?? EmptyReadOnly.Collection; - - /// - /// Seen Exceptions that we try to failover on before this was thrown. - /// - // ReSharper disable once MemberCanBePrivate.Global - public IReadOnlyCollection SeenExceptions { get; } - } - - + // ReSharper disable once MemberCanBePrivate.Global + public IReadOnlyCollection SeenExceptions { get; } } diff --git a/src/Elastic.Transport/Extensions/EmptyReadonly.cs b/src/Elastic.Transport/Extensions/EmptyReadonly.cs index dd93aca..1695d3b 100644 --- a/src/Elastic.Transport/Extensions/EmptyReadonly.cs +++ b/src/Elastic.Transport/Extensions/EmptyReadonly.cs @@ -6,27 +6,26 @@ using System.Collections.ObjectModel; using System.Linq; -namespace Elastic.Transport.Extensions -{ - internal static class EmptyReadOnlyExtensions - { - public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable enumerable) => - enumerable == null ? EmptyReadOnly.Collection : new ReadOnlyCollection(enumerable.ToList()); +namespace Elastic.Transport.Extensions; - public static IReadOnlyCollection ToReadOnlyCollection(this IList enumerable) => - enumerable == null || enumerable.Count == 0 ? EmptyReadOnly.Collection : new ReadOnlyCollection(enumerable); - } +internal static class EmptyReadOnlyExtensions +{ + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable enumerable) => + enumerable == null ? EmptyReadOnly.Collection : new ReadOnlyCollection(enumerable.ToList()); + public static IReadOnlyCollection ToReadOnlyCollection(this IList enumerable) => + enumerable == null || enumerable.Count == 0 ? EmptyReadOnly.Collection : new ReadOnlyCollection(enumerable); +} - internal static class EmptyReadOnly - { - public static readonly IReadOnlyCollection Collection = new ReadOnlyCollection(new TElement[0]); - public static readonly IReadOnlyList List = new List(); - } - internal static class EmptyReadOnly - { - public static readonly IReadOnlyDictionary Dictionary = new ReadOnlyDictionary(new Dictionary(0)); - } +internal static class EmptyReadOnly +{ + public static readonly IReadOnlyCollection Collection = new ReadOnlyCollection(new TElement[0]); + public static readonly IReadOnlyList List = new List(); +} +internal static class EmptyReadOnly +{ + public static readonly IReadOnlyDictionary Dictionary = new ReadOnlyDictionary(new Dictionary(0)); } + diff --git a/src/Elastic.Transport/Extensions/EnumExtensions.cs b/src/Elastic.Transport/Extensions/EnumExtensions.cs index 2960e82..2c42aed 100644 --- a/src/Elastic.Transport/Extensions/EnumExtensions.cs +++ b/src/Elastic.Transport/Extensions/EnumExtensions.cs @@ -7,67 +7,66 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Elastic.Transport.Extensions +namespace Elastic.Transport.Extensions; + +/// +/// Cached to string extension method for enums. This is public because we expect most clients to need this. +/// This takes into account +/// +public static class EnumExtensions { - /// - /// Cached to string extension method for enums. This is public because we expect most clients to need this. - /// This takes into account - /// - public static class EnumExtensions + internal static string GetStringValue(this HttpMethod enumValue) { - internal static string GetStringValue(this HttpMethod enumValue) + switch (enumValue) { - switch (enumValue) - { - case HttpMethod.GET: return "GET"; - case HttpMethod.POST: return "POST"; - case HttpMethod.PUT: return "PUT"; - case HttpMethod.DELETE: return "DELETE"; - case HttpMethod.HEAD: return "HEAD"; - default: - throw new ArgumentOutOfRangeException(nameof(enumValue), enumValue, null); - } + case HttpMethod.GET: return "GET"; + case HttpMethod.POST: return "POST"; + case HttpMethod.PUT: return "PUT"; + case HttpMethod.DELETE: return "DELETE"; + case HttpMethod.HEAD: return "HEAD"; + default: + throw new ArgumentOutOfRangeException(nameof(enumValue), enumValue, null); } + } - private static readonly ConcurrentDictionary> EnumStringResolvers = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> EnumStringResolvers = new ConcurrentDictionary>(); - /// - /// Returns the string representation of the enum taking into account - /// - public static string GetStringValue(this Enum e) + /// + /// Returns the string representation of the enum taking into account + /// + public static string GetStringValue(this Enum e) + { + var type = e.GetType(); + var resolver = EnumStringResolvers.GetOrAdd(type, t => GetEnumStringResolver(t)); + return resolver(e); + } + + private static Func GetEnumStringResolver(Type type) + { + var values = Enum.GetValues(type); + var dictionary = new Dictionary(values.Length); + for (var index = 0; index < values.Length; index++) { - var type = e.GetType(); - var resolver = EnumStringResolvers.GetOrAdd(type, t => GetEnumStringResolver(t)); - return resolver(e); + var value = values.GetValue(index); + var info = type.GetField(value.ToString()); + var da = (EnumMemberAttribute[])info.GetCustomAttributes(typeof(EnumMemberAttribute), false); + var stringValue = da.Length > 0 ? da[0].Value : Enum.GetName(type, value); + dictionary.Add((Enum)value, stringValue); } - private static Func GetEnumStringResolver(Type type) + var isFlag = type.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0; + return e => { - var values = Enum.GetValues(type); - var dictionary = new Dictionary(values.Length); - for (var index = 0; index < values.Length; index++) - { - var value = values.GetValue(index); - var info = type.GetField(value.ToString()); - var da = (EnumMemberAttribute[])info.GetCustomAttributes(typeof(EnumMemberAttribute), false); - var stringValue = da.Length > 0 ? da[0].Value : Enum.GetName(type, value); - dictionary.Add((Enum)value, stringValue); - } + if (!isFlag) return dictionary[e]; - var isFlag = type.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0; - return e => + var list = new List(); + foreach (var kv in dictionary) { - if (!isFlag) return dictionary[e]; - - var list = new List(); - foreach (var kv in dictionary) - { - if (e.HasFlag(kv.Key)) - list.Add(kv.Value); - } + if (e.HasFlag(kv.Key)) + list.Add(kv.Value); + } - return string.Join(",", list); - }; - } + return string.Join(",", list); + }; } } diff --git a/src/Elastic.Transport/Extensions/Extensions.cs b/src/Elastic.Transport/Extensions/Extensions.cs index 8beffc6..cbaf0e4 100644 --- a/src/Elastic.Transport/Extensions/Extensions.cs +++ b/src/Elastic.Transport/Extensions/Extensions.cs @@ -9,105 +9,104 @@ using System.Linq; using System.Text; -namespace Elastic.Transport.Extensions +namespace Elastic.Transport.Extensions; + +internal static class Extensions { - internal static class Extensions + [Obsolete("Please use the overload placing the enumerated array as out")] + internal static bool HasAny(this ICollection list) => list != null && list.Any(); + + internal static bool HasAny(this IEnumerable list, out T[] enumerated) { - [Obsolete("Please use the overload placing the enumerated array as out")] - internal static bool HasAny(this ICollection list) => list != null && list.Any(); + enumerated = list == null ? null : (list as T[] ?? list.ToArray()); + return enumerated != null && enumerated.Length > 0; + } - internal static bool HasAny(this IEnumerable list, out T[] enumerated) - { - enumerated = list == null ? null : (list as T[] ?? list.ToArray()); - return enumerated != null && enumerated.Length > 0; - } + internal static IEnumerable DistinctByCustom(this IEnumerable items, Func property) => + items.GroupBy(property).Select(x => x.First()); - internal static IEnumerable DistinctByCustom(this IEnumerable items, Func property) => - items.GroupBy(property).Select(x => x.First()); + internal static void ThrowIfEmpty(this IEnumerable @object, string parameterName) + { + var enumerated = @object == null ? null : (@object as T[] ?? @object.ToArray()); + enumerated.ThrowIfNull(parameterName); + if (!enumerated!.Any()) + throw new ArgumentException("Argument can not be an empty collection", parameterName); + } + internal static void ThrowIfNull(this T value, string name) where T : class + { + if (value == null) + throw new ArgumentNullException(name); + } - internal static void ThrowIfEmpty(this IEnumerable @object, string parameterName) - { - var enumerated = @object == null ? null : (@object as T[] ?? @object.ToArray()); - enumerated.ThrowIfNull(parameterName); - if (!enumerated!.Any()) - throw new ArgumentException("Argument can not be an empty collection", parameterName); - } - internal static void ThrowIfNull(this T value, string name) where T : class - { - if (value == null) - throw new ArgumentNullException(name); - } + internal static bool IsNullOrEmpty(this string value) => string.IsNullOrEmpty(value); - internal static bool IsNullOrEmpty(this string value) => string.IsNullOrEmpty(value); + internal static string Utf8String(this byte[] bytes) => bytes == null ? null : Encoding.UTF8.GetString(bytes, 0, bytes.Length); - internal static string Utf8String(this byte[] bytes) => bytes == null ? null : Encoding.UTF8.GetString(bytes, 0, bytes.Length); + internal static string Utf8String(this MemoryStream ms) + { + if (ms is null) + return null; - internal static string Utf8String(this MemoryStream ms) - { - if (ms is null) - return null; + if (!ms.TryGetBuffer(out var buffer) || buffer.Array is null) + return Encoding.UTF8.GetString(ms.ToArray()); - if (!ms.TryGetBuffer(out var buffer) || buffer.Array is null) - return Encoding.UTF8.GetString(ms.ToArray()); + return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); + } - return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); - } + internal static byte[] Utf8Bytes(this string s) => s.IsNullOrEmpty() ? null : Encoding.UTF8.GetBytes(s); - internal static byte[] Utf8Bytes(this string s) => s.IsNullOrEmpty() ? null : Encoding.UTF8.GetBytes(s); + private const long MillisecondsInAWeek = MillisecondsInADay * 7; + private const long MillisecondsInADay = MillisecondsInAnHour * 24; + private const long MillisecondsInAnHour = MillisecondsInAMinute * 60; + private const long MillisecondsInAMinute = MillisecondsInASecond * 60; + private const long MillisecondsInASecond = 1000; - private const long MillisecondsInAWeek = MillisecondsInADay * 7; - private const long MillisecondsInADay = MillisecondsInAnHour * 24; - private const long MillisecondsInAnHour = MillisecondsInAMinute * 60; - private const long MillisecondsInAMinute = MillisecondsInASecond * 60; - private const long MillisecondsInASecond = 1000; + internal static string ToTimeUnit(this TimeSpan timeSpan) + { + var ms = timeSpan.TotalMilliseconds; + string interval; + double factor; - internal static string ToTimeUnit(this TimeSpan timeSpan) + if (ms >= MillisecondsInAWeek) { - var ms = timeSpan.TotalMilliseconds; - string interval; - double factor; - - if (ms >= MillisecondsInAWeek) - { - factor = ms / MillisecondsInAWeek; - interval = "w"; - } - else if (ms >= MillisecondsInADay) - { - factor = ms / MillisecondsInADay; - interval = "d"; - } - else if (ms >= MillisecondsInAnHour) - { - factor = ms / MillisecondsInAnHour; - interval = "h"; - } - else if (ms >= MillisecondsInAMinute) - { - factor = ms / MillisecondsInAMinute; - interval = "m"; - } - else if (ms >= MillisecondsInASecond) - { - factor = ms / MillisecondsInASecond; - interval = "s"; - } - else - { - factor = ms; - interval = "ms"; - } - - return factor.ToString("0.##", CultureInfo.InvariantCulture) + interval; + factor = ms / MillisecondsInAWeek; + interval = "w"; } - - internal static Exception AsAggregateOrFirst(this IEnumerable exceptions) + else if (ms >= MillisecondsInADay) { - var es = exceptions as Exception[] ?? exceptions?.ToArray(); - if (es == null || es.Length == 0) return null; - - return es.Length == 1 ? es[0] : new AggregateException(es); + factor = ms / MillisecondsInADay; + interval = "d"; + } + else if (ms >= MillisecondsInAnHour) + { + factor = ms / MillisecondsInAnHour; + interval = "h"; + } + else if (ms >= MillisecondsInAMinute) + { + factor = ms / MillisecondsInAMinute; + interval = "m"; + } + else if (ms >= MillisecondsInASecond) + { + factor = ms / MillisecondsInASecond; + interval = "s"; + } + else + { + factor = ms; + interval = "ms"; } + return factor.ToString("0.##", CultureInfo.InvariantCulture) + interval; + } + + internal static Exception AsAggregateOrFirst(this IEnumerable exceptions) + { + var es = exceptions as Exception[] ?? exceptions?.ToArray(); + if (es == null || es.Length == 0) return null; + + return es.Length == 1 ? es[0] : new AggregateException(es); } + } diff --git a/src/Elastic.Transport/Extensions/Fluent.cs b/src/Elastic.Transport/Extensions/Fluent.cs index 95e3a10..7f86db8 100644 --- a/src/Elastic.Transport/Extensions/Fluent.cs +++ b/src/Elastic.Transport/Extensions/Fluent.cs @@ -4,15 +4,14 @@ using System; -namespace Elastic.Transport.Extensions +namespace Elastic.Transport.Extensions; + +internal static class Fluent { - internal static class Fluent + internal static TDescriptor Assign(TDescriptor self, TValue value, Action assign) + where TDescriptor : class, TInterface { - internal static TDescriptor Assign(TDescriptor self, TValue value, Action assign) - where TDescriptor : class, TInterface - { - assign(self, value); - return self; - } + assign(self, value); + return self; } } diff --git a/src/Elastic.Transport/Extensions/NameValueCollectionExtensions.cs b/src/Elastic.Transport/Extensions/NameValueCollectionExtensions.cs index 1c40413..704fc52 100644 --- a/src/Elastic.Transport/Extensions/NameValueCollectionExtensions.cs +++ b/src/Elastic.Transport/Extensions/NameValueCollectionExtensions.cs @@ -9,82 +9,81 @@ using System.Linq; using System.Text; -namespace Elastic.Transport.Extensions +namespace Elastic.Transport.Extensions; + +internal static class NameValueCollectionExtensions { - internal static class NameValueCollectionExtensions + private const int MaxCharsOnStack = 256; // 512 bytes + + internal static string ToQueryString(this NameValueCollection nv) { - private const int MaxCharsOnStack = 256; // 512 bytes + if (nv == null || nv.AllKeys.Length == 0) return string.Empty; - internal static string ToQueryString(this NameValueCollection nv) + var maxLength = 1 + nv.AllKeys.Length - 1; // account for '?', and any required '&' chars + foreach (var key in nv.AllKeys) { - if (nv == null || nv.AllKeys.Length == 0) return string.Empty; + var bytes = Encoding.UTF8.GetByteCount(key) + Encoding.UTF8.GetByteCount(nv[key] ?? string.Empty); + var maxEncodedSize = bytes * 3; // worst case, assume all bytes are URL escaped to 3 chars + maxLength += 1 + maxEncodedSize; // '=' + encoded chars + } - var maxLength = 1 + nv.AllKeys.Length - 1; // account for '?', and any required '&' chars - foreach (var key in nv.AllKeys) - { - var bytes = Encoding.UTF8.GetByteCount(key) + Encoding.UTF8.GetByteCount(nv[key] ?? string.Empty); - var maxEncodedSize = bytes * 3; // worst case, assume all bytes are URL escaped to 3 chars - maxLength += 1 + maxEncodedSize; // '=' + encoded chars - } + // prefer stack allocated array for short lengths + // note: renting for larger lengths is slightly more efficient since no zeroing occurs + char[] rentedFromPool = null; + var buffer = maxLength > MaxCharsOnStack + ? rentedFromPool = ArrayPool.Shared.Rent(maxLength) + : stackalloc char[maxLength]; - // prefer stack allocated array for short lengths - // note: renting for larger lengths is slightly more efficient since no zeroing occurs - char[] rentedFromPool = null; - var buffer = maxLength > MaxCharsOnStack - ? rentedFromPool = ArrayPool.Shared.Rent(maxLength) - : stackalloc char[maxLength]; + try + { + var position = 0; + buffer[position++] = '?'; - try + foreach (var key in nv.AllKeys) { - var position = 0; - buffer[position++] = '?'; - - foreach (var key in nv.AllKeys) - { - if (position != 1) - buffer[position++] = '&'; - - var escapedKey = Uri.EscapeDataString(key); - escapedKey.AsSpan().CopyTo(buffer.Slice(position)); - position += escapedKey.Length; + if (position != 1) + buffer[position++] = '&'; - var value = nv[key]; + var escapedKey = Uri.EscapeDataString(key); + escapedKey.AsSpan().CopyTo(buffer.Slice(position)); + position += escapedKey.Length; - if (value.IsNullOrEmpty()) continue; + var value = nv[key]; - buffer[position++] = '='; - var escapedValue = Uri.EscapeDataString(value); - escapedValue.AsSpan().CopyTo(buffer.Slice(position)); - position += escapedValue.Length; - } + if (value.IsNullOrEmpty()) continue; - return buffer.Slice(0, position).ToString(); + buffer[position++] = '='; + var escapedValue = Uri.EscapeDataString(value); + escapedValue.AsSpan().CopyTo(buffer.Slice(position)); + position += escapedValue.Length; } - finally - { - if (rentedFromPool != null) - ArrayPool.Shared.Return(rentedFromPool, clearArray: false); - } - } - internal static void UpdateFromDictionary(this NameValueCollection queryString, Dictionary queryStringUpdates, UrlFormatter provider) + return buffer.Slice(0, position).ToString(); + } + finally { - if (queryString == null || queryString.Count < 0) return; - if (queryStringUpdates == null || queryStringUpdates.Count < 0) return; + if (rentedFromPool != null) + ArrayPool.Shared.Return(rentedFromPool, clearArray: false); + } + } + + internal static void UpdateFromDictionary(this NameValueCollection queryString, Dictionary queryStringUpdates, UrlFormatter provider) + { + if (queryString == null || queryString.Count < 0) return; + if (queryStringUpdates == null || queryStringUpdates.Count < 0) return; - foreach (var kv in queryStringUpdates.Where(kv => !kv.Key.IsNullOrEmpty())) + foreach (var kv in queryStringUpdates.Where(kv => !kv.Key.IsNullOrEmpty())) + { + if (kv.Value == null) { - if (kv.Value == null) - { - queryString.Remove(kv.Key); - continue; - } - var resolved = provider.CreateString(kv.Value); - if (resolved != null) - queryString[kv.Key] = resolved; - else - queryString.Remove(kv.Key); + queryString.Remove(kv.Key); + continue; } + var resolved = provider.CreateString(kv.Value); + if (resolved != null) + queryString[kv.Key] = resolved; + else + queryString.Remove(kv.Key); } } } diff --git a/src/Elastic.Transport/Extensions/StringExtensions.cs b/src/Elastic.Transport/Extensions/StringExtensions.cs new file mode 100644 index 0000000..92a48b6 --- /dev/null +++ b/src/Elastic.Transport/Extensions/StringExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Transport; + +internal static class StringExtensions +{ + internal static string ToCamelCase(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + + if (!char.IsUpper(s[0])) + return s; + + var camelCase = char.ToLowerInvariant(s[0]).ToString(); + if (s.Length > 1) + camelCase += s.Substring(1); + + return camelCase; + } +} diff --git a/src/Elastic.Transport/HttpTransport.cs b/src/Elastic.Transport/HttpTransport.cs new file mode 100644 index 0000000..6707534 --- /dev/null +++ b/src/Elastic.Transport/HttpTransport.cs @@ -0,0 +1,46 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Transport; + +/// +/// Represents a transport you can call requests, it is recommended to implement +/// +public abstract class HttpTransport +{ + /// + /// Perform a request into the products cluster using 's workflow. + /// + public abstract TResponse Request( + HttpMethod method, + string path, + PostData data = null, + RequestParameters requestParameters = null) + where TResponse : TransportResponse, new(); + + /// + public abstract Task RequestAsync( + HttpMethod method, + string path, + PostData data = null, + RequestParameters requestParameters = null, + CancellationToken cancellationToken = default) + where TResponse : TransportResponse, new(); +} + +/// +/// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on +/// different nodes +/// +public abstract class HttpTransport : HttpTransport + where TConfiguration : class, ITransportConfiguration +{ + /// + /// The in use by this transport instance + /// + public abstract TConfiguration Settings { get; } +} diff --git a/src/Elastic.Transport/ITransport.cs b/src/Elastic.Transport/ITransport.cs deleted file mode 100644 index 7d92335..0000000 --- a/src/Elastic.Transport/ITransport.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Threading; -using System.Threading.Tasks; - -namespace Elastic.Transport -{ - /// - /// Represents a transport you can call requests, it is recommended to implement - /// - public interface ITransport - { - /// - /// Perform a request into the products cluster using 's workflow. - /// - TResponse Request( - HttpMethod method, - string path, - PostData data = null, - IRequestParameters requestParameters = null) - where TResponse : class, ITransportResponse, new(); - - /// - Task RequestAsync( - HttpMethod method, - string path, - PostData data = null, - IRequestParameters requestParameters = null, - CancellationToken cancellationToken = default) - where TResponse : class, ITransportResponse, new(); - } - - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different nodes - /// - public interface ITransport : ITransport - where TConfiguration : ITransportConfiguration - { - /// - /// The in use by this transport instance - /// - TConfiguration Settings { get; } - } -} diff --git a/src/Elastic.Transport/Products/DefaultProductRegistration.cs b/src/Elastic.Transport/Products/DefaultProductRegistration.cs index 77fd01c..6c2dcb7 100644 --- a/src/Elastic.Transport/Products/DefaultProductRegistration.cs +++ b/src/Elastic.Transport/Products/DefaultProductRegistration.cs @@ -7,84 +7,82 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport.Products +namespace Elastic.Transport.Products; + +/// +/// A default non-descriptive product registration that does not support sniffing and pinging. +/// Can be used to connect to unknown services before they develop their own +/// implementations +/// +public sealed class DefaultProductRegistration : ProductRegistration { + private readonly HeadersList _headers = new(); + private readonly MetaHeaderProvider _metaHeaderProvider; + /// - /// A default non-descriptive product registration that does not support sniffing and pinging. - /// Can be used to connect to unknown services before they develop their own - /// implementations + /// /// - public sealed class ProductRegistration : IProductRegistration - { - private readonly HeadersList _headers = new(); - private readonly MetaHeaderProvider _metaHeaderProvider; - - /// - /// - /// - public ProductRegistration() => _metaHeaderProvider = new DefaultMetaHeaderProvider(typeof(ITransport), "et"); + public DefaultProductRegistration() => _metaHeaderProvider = new DefaultMetaHeaderProvider(typeof(HttpTransport), "et"); - /// A static instance of to promote reuse - public static ProductRegistration Default { get; } = new ProductRegistration(); + /// A static instance of to promote reuse + public static DefaultProductRegistration Default { get; } = new DefaultProductRegistration(); - /// - public string Name { get; } = "elastic-transport-net"; + /// + public override string Name { get; } = "elastic-transport-net"; - /// - public bool SupportsPing { get; } = false; + /// + public override bool SupportsPing { get; } = false; - /// - public bool SupportsSniff { get; } = false; + /// + public override bool SupportsSniff { get; } = false; - /// - public int SniffOrder(Node node) => -1; + /// + public override int SniffOrder(Node node) => -1; - /// - public bool NodePredicate(Node node) => true; + /// + public override bool NodePredicate(Node node) => true; - /// - public HeadersList ResponseHeadersToParse => _headers; + /// + public override HeadersList ResponseHeadersToParse => _headers; - /// - public MetaHeaderProvider MetaHeaderProvider => _metaHeaderProvider; + /// + public override MetaHeaderProvider MetaHeaderProvider => _metaHeaderProvider; - /// - public ResponseBuilder ResponseBuilder => new DefaultResponseBuilder(); + /// + public override ResponseBuilder ResponseBuilder => new DefaultResponseBuilder(); - /// - public bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) => - statusCode >= 200 && statusCode < 300; + /// + public override bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) => + statusCode >= 200 && statusCode < 300; - /// > - public bool TryGetServerErrorReason(TResponse response, out string reason) - where TResponse : ITransportResponse - { - reason = null; - return false; - } + /// > + public override bool TryGetServerErrorReason(TResponse response, out string reason) + { + reason = null; + return false; + } - /// - public RequestData CreateSniffRequestData(Node node, IRequestConfiguration requestConfiguration, ITransportConfiguration settings, IMemoryStreamFactory memoryStreamFactory) => - throw new NotImplementedException(); + /// + public override RequestData CreateSniffRequestData(Node node, IRequestConfiguration requestConfiguration, ITransportConfiguration settings, MemoryStreamFactory memoryStreamFactory) => + throw new NotImplementedException(); - /// - public Task>> SniffAsync(ITransportClient transportClient, bool forceSsl, RequestData requestData, CancellationToken cancellationToken) => - throw new NotImplementedException(); + /// + public override Task>> SniffAsync(TransportClient transportClient, bool forceSsl, RequestData requestData, CancellationToken cancellationToken) => + throw new NotImplementedException(); - /// - public Tuple> Sniff(ITransportClient connection, bool forceSsl, RequestData requestData) => - throw new NotImplementedException(); + /// + public override Tuple> Sniff(TransportClient connection, bool forceSsl, RequestData requestData) => + throw new NotImplementedException(); - /// - public RequestData CreatePingRequestData(Node node, RequestConfiguration requestConfiguration, ITransportConfiguration global, IMemoryStreamFactory memoryStreamFactory) => - throw new NotImplementedException(); + /// + public override RequestData CreatePingRequestData(Node node, RequestConfiguration requestConfiguration, ITransportConfiguration global, MemoryStreamFactory memoryStreamFactory) => + throw new NotImplementedException(); - /// - public Task PingAsync(ITransportClient connection, RequestData pingData, CancellationToken cancellationToken) => - throw new NotImplementedException(); + /// + public override Task PingAsync(TransportClient connection, RequestData pingData, CancellationToken cancellationToken) => + throw new NotImplementedException(); - /// - public IApiCallDetails Ping(ITransportClient connection, RequestData pingData) => - throw new NotImplementedException(); - } + /// + public override TransportResponse Ping(TransportClient connection, RequestData pingData) => + throw new NotImplementedException(); } diff --git a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchErrorExtensions.cs b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchErrorExtensions.cs index 01fafd7..040dcf4 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchErrorExtensions.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchErrorExtensions.cs @@ -4,51 +4,50 @@ using System.Text; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// +/// Extends the builtin responses with parsing for +/// +public static class ElasticsearchErrorExtensions { + /// Try to parse an Elasticsearch + public static bool TryGetElasticsearchServerError(this StringResponse response, out ElasticsearchServerError serverError) + { + serverError = null; + if (string.IsNullOrEmpty(response.Body) || response.ApiCallDetails.ResponseMimeType != RequestData.MimeType) + return false; + + var settings = response.ApiCallDetails.TransportConfiguration; + using var stream = settings.MemoryStreamFactory.Create(Encoding.UTF8.GetBytes(response.Body)); + return ElasticsearchServerError.TryCreate(stream, out serverError); + } + + /// Try to parse an Elasticsearch + public static bool TryGetElasticsearchServerError(this BytesResponse response, out ElasticsearchServerError serverError) + { + serverError = null; + if (response.Body == null || response.Body.Length == 0 || response.ApiCallDetails.ResponseMimeType != RequestData.MimeType) + return false; + + var settings = response.ApiCallDetails.TransportConfiguration; + using var stream = settings.MemoryStreamFactory.Create(response.Body); + return ElasticsearchServerError.TryCreate(stream, out serverError); + } + /// - /// Extends the builtin responses with parsing for + /// Try to parse an Elasticsearch , this only works if + /// gives us access to /// - public static class ElasticsearchErrorExtensions + public static bool TryGetElasticsearchServerError(this TransportResponse response, out ElasticsearchServerError serverError) { - /// Try to parse an Elasticsearch - public static bool TryGetElasticsearchServerError(this StringResponse response, out ServerError serverError) - { - serverError = null; - if (string.IsNullOrEmpty(response.Body) || response.ResponseMimeType != RequestData.MimeType) - return false; - - var settings = response.ApiCall.TransportConfiguration; - using var stream = settings.MemoryStreamFactory.Create(Encoding.UTF8.GetBytes(response.Body)); - return ServerError.TryCreate(stream, out serverError); - } - - /// Try to parse an Elasticsearch - public static bool TryGetElasticsearchServerError(this BytesResponse response, out ServerError serverError) - { - serverError = null; - if (response.Body == null || response.Body.Length == 0 || response.ResponseMimeType != RequestData.MimeType) - return false; - - var settings = response.ApiCall.TransportConfiguration; - using var stream = settings.MemoryStreamFactory.Create(response.Body); - return ServerError.TryCreate(stream, out serverError); - } - - /// - /// Try to parse an Elasticsearch , this only works if - /// gives us access to - /// - public static bool TryGetElasticsearchServerError(this ITransportResponse response, out ServerError serverError) - { - serverError = null; - var bytes = response.ApiCall.ResponseBodyInBytes; - if (bytes == null || response.ApiCall.ResponseMimeType != RequestData.MimeType) - return false; - - var settings = response.ApiCall.TransportConfiguration; - using var stream = settings.MemoryStreamFactory.Create(bytes); - return ServerError.TryCreate(stream, out serverError); - } + serverError = null; + var bytes = response.ApiCallDetails.ResponseBodyInBytes; + if (bytes == null || response.ApiCallDetails.ResponseMimeType != RequestData.MimeType) + return false; + + var settings = response.ApiCallDetails.TransportConfiguration; + using var stream = settings.MemoryStreamFactory.Create(bytes); + return ElasticsearchServerError.TryCreate(stream, out serverError); } } diff --git a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchNodeFeatures.cs b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchNodeFeatures.cs index e2c66eb..d1b852a 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchNodeFeatures.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchNodeFeatures.cs @@ -5,36 +5,35 @@ using System.Collections.Generic; using System.Linq; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// +/// Encodes the features will register on +/// . These static strings make it easier to inspect if features are enabled +/// using +/// +public static class ElasticsearchNodeFeatures { - /// - /// Encodes the features will register on - /// . These static strings make it easier to inspect if features are enabled - /// using - /// - public static class ElasticsearchNodeFeatures - { - /// Indicates whether this node holds data, defaults to true when unknown/unspecified - public const string HoldsData = "node.data"; - /// Whether HTTP is enabled on the node or not - public const string HttpEnabled = "node.http"; - /// Indicates whether this node is allowed to run ingest pipelines, defaults to true when unknown/unspecified - public const string IngestEnabled = "node.ingest"; - /// Indicates whether this node is master eligible, defaults to true when unknown/unspecified - public const string MasterEligible = "node.master"; + /// Indicates whether this node holds data, defaults to true when unknown/unspecified + public const string HoldsData = "node.data"; + /// Whether HTTP is enabled on the node or not + public const string HttpEnabled = "node.http"; + /// Indicates whether this node is allowed to run ingest pipelines, defaults to true when unknown/unspecified + public const string IngestEnabled = "node.ingest"; + /// Indicates whether this node is master eligible, defaults to true when unknown/unspecified + public const string MasterEligible = "node.master"; - /// The default collection of features, which enables ALL Features - public static readonly IReadOnlyCollection Default = - new[] { HoldsData, MasterEligible, IngestEnabled, HttpEnabled }.ToList().AsReadOnly(); + /// The default collection of features, which enables ALL Features + public static readonly IReadOnlyCollection Default = + new[] { HoldsData, MasterEligible, IngestEnabled, HttpEnabled }.ToList().AsReadOnly(); - /// The node only has the and features - public static readonly IReadOnlyCollection MasterEligibleOnly = - new[] { MasterEligible, HttpEnabled }.ToList().AsReadOnly(); + /// The node only has the and features + public static readonly IReadOnlyCollection MasterEligibleOnly = + new[] { MasterEligible, HttpEnabled }.ToList().AsReadOnly(); - /// The node has all features EXCEPT - // ReSharper disable once UnusedMember.Global - public static readonly IReadOnlyCollection NotMasterEligible = - Default.Except(new[] { MasterEligible }).ToList().AsReadOnly(); + /// The node has all features EXCEPT + // ReSharper disable once UnusedMember.Global + public static readonly IReadOnlyCollection NotMasterEligible = + Default.Except(new[] { MasterEligible }).ToList().AsReadOnly(); - } } diff --git a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchProductRegistration.cs b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchProductRegistration.cs index 31e8617..eab5560 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchProductRegistration.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchProductRegistration.cs @@ -9,144 +9,148 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// +/// An implementation of that fills in the bespoke implementations +/// for Elasticsearch so that knows how to ping and sniff if we setup +/// to talk to Elasticsearch +/// +public class ElasticsearchProductRegistration : ProductRegistration { + private readonly HeadersList _headers; + private readonly MetaHeaderProvider _metaHeaderProvider; + + /// + /// Create a new instance of the Elasticsearch product registration. + /// + public ElasticsearchProductRegistration() => _headers = new HeadersList("warning"); + /// - /// An implementation of that fills in the bespoke implementations - /// for Elasticsearch so that knows how to ping and sniff if we setup - /// to talk to Elasticsearch + /// /// - public class ElasticsearchProductRegistration : IProductRegistration + /// + public ElasticsearchProductRegistration(Type markerType) : this() => _metaHeaderProvider = new DefaultMetaHeaderProvider(markerType, "es"); + + /// A static instance of to promote reuse + public static ProductRegistration Default { get; } = new ElasticsearchProductRegistration(); + + /// + public override string Name { get; } = "elasticsearch-net"; + + /// + public override bool SupportsPing { get; } = true; + + /// + public override bool SupportsSniff { get; } = true; + + /// + public override HeadersList ResponseHeadersToParse => _headers; + + /// + public override MetaHeaderProvider MetaHeaderProvider => _metaHeaderProvider; + + /// + public override ResponseBuilder ResponseBuilder => new ElasticsearchResponseBuilder(); + + /// Exposes the path used for sniffing in Elasticsearch + public const string SniffPath = "_nodes/http,settings"; + + /// + /// Implements an ordering that prefers master eligible nodes when attempting to sniff the + /// + /// + public override int SniffOrder(Node node) => + node.HasFeature(ElasticsearchNodeFeatures.MasterEligible) ? node.Uri.Port : int.MaxValue; + + /// + /// If we know that a node is a master eligible node that hold no data it is excluded from regular + /// API calls. They are considered for ping and sniff requests. + /// + public override bool NodePredicate(Node node) => + // skip master only nodes (holds no data and is master eligible) + !(node.HasFeature(ElasticsearchNodeFeatures.MasterEligible) && + !node.HasFeature(ElasticsearchNodeFeatures.HoldsData)); + + /// + public override bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) => + statusCode >= 200 && statusCode < 300; + + /// > + public override bool TryGetServerErrorReason(TResponse response, out string reason) { - private readonly HeadersList _headers; - private readonly MetaHeaderProvider _metaHeaderProvider; - - /// - /// Create a new instance of the Elasticsearch product registration. - /// - public ElasticsearchProductRegistration() => _headers = new HeadersList("warning"); - - /// - /// - /// - /// - public ElasticsearchProductRegistration(Type markerType) : this() => _metaHeaderProvider = new DefaultMetaHeaderProvider(markerType, "es"); - - /// A static instance of to promote reuse - public static IProductRegistration Default { get; } = new ElasticsearchProductRegistration(); - - /// - public string Name { get; } = "elasticsearch-net"; - - /// - public bool SupportsPing { get; } = true; - - /// - public bool SupportsSniff { get; } = true; - - /// - public HeadersList ResponseHeadersToParse => _headers; - - /// - public MetaHeaderProvider MetaHeaderProvider => _metaHeaderProvider; - - /// - public ResponseBuilder ResponseBuilder => new ElasticsearchResponseBuilder(); - - /// Exposes the path used for sniffing in Elasticsearch - public const string SniffPath = "_nodes/http,settings"; - - /// - /// Implements an ordering that prefers master eligible nodes when attempting to sniff the - /// - /// - public int SniffOrder(Node node) => - node.HasFeature(ElasticsearchNodeFeatures.MasterEligible) ? node.Uri.Port : int.MaxValue; - - /// - /// If we know that a node is a master eligible node that hold no data it is excluded from regular - /// API calls. They are considered for ping and sniff requests. - /// - public bool NodePredicate(Node node) => - // skip master only nodes (holds no data and is master eligible) - !(node.HasFeature(ElasticsearchNodeFeatures.MasterEligible) && - !node.HasFeature(ElasticsearchNodeFeatures.HoldsData)); - - /// - public virtual bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) => - statusCode >= 200 && statusCode < 300; - - /// > - public virtual bool TryGetServerErrorReason(TResponse response, out string reason) - where TResponse : ITransportResponse - { - reason = null; - if (response is StringResponse s && s.TryGetElasticsearchServerError(out var e)) reason = e.Error?.ToString(); - else if (response is BytesResponse b && b.TryGetElasticsearchServerError(out e)) reason = e.Error?.ToString(); - else if (response.TryGetElasticsearchServerError(out e)) reason = e.Error?.ToString(); - return e != null; - } - - /// - public RequestData CreateSniffRequestData(Node node, IRequestConfiguration requestConfiguration, - ITransportConfiguration settings, - IMemoryStreamFactory memoryStreamFactory - ) - { - var requestParameters = new RequestParameters - { - QueryString = {{"timeout", requestConfiguration.PingTimeout}, {"flat_settings", true},} - }; - return new RequestData(HttpMethod.GET, SniffPath, null, settings, requestParameters, memoryStreamFactory) - { - Node = node - }; - } - - /// - public async Task>> SniffAsync(ITransportClient transportClient, - bool forceSsl, RequestData requestData, CancellationToken cancellationToken) + reason = null; + if (response is StringResponse s && s.TryGetElasticsearchServerError(out var e)) reason = e.Error?.ToString(); + else if (response is BytesResponse b && b.TryGetElasticsearchServerError(out e)) reason = e.Error?.ToString(); + else if (response.TryGetElasticsearchServerError(out e)) reason = e.Error?.ToString(); + return e != null; + } + + /// + public override RequestData CreateSniffRequestData(Node node, IRequestConfiguration requestConfiguration, + ITransportConfiguration settings, + MemoryStreamFactory memoryStreamFactory + ) + { + var requestParameters = new DefaultRequestParameters { - var response = await transportClient.RequestAsync(requestData, cancellationToken) - .ConfigureAwait(false); - var nodes = response.ToNodes(forceSsl); - return Tuple.Create>(response, - new ReadOnlyCollection(nodes.ToArray())); - } - - /// - public Tuple> Sniff(ITransportClient transportClient, bool forceSsl, - RequestData requestData) + QueryString = {{"timeout", requestConfiguration.PingTimeout}, {"flat_settings", true},} + }; + return new RequestData(HttpMethod.GET, SniffPath, null, settings, requestParameters, memoryStreamFactory) { - var response = transportClient.Request(requestData); - var nodes = response.ToNodes(forceSsl); - return Tuple.Create>(response, - new ReadOnlyCollection(nodes.ToArray())); - } - - /// - public RequestData CreatePingRequestData(Node node, RequestConfiguration requestConfiguration, - ITransportConfiguration global, - IMemoryStreamFactory memoryStreamFactory - ) + Node = node + }; + } + + /// + public override async Task>> SniffAsync(TransportClient transportClient, + bool forceSsl, RequestData requestData, CancellationToken cancellationToken) + { + var response = await transportClient.RequestAsync(requestData, cancellationToken) + .ConfigureAwait(false); + var nodes = response.ToNodes(forceSsl); + return Tuple.Create>(response, + new ReadOnlyCollection(nodes.ToArray())); + } + + /// + public override Tuple> Sniff(TransportClient transportClient, bool forceSsl, + RequestData requestData) + { + var response = transportClient.Request(requestData); + var nodes = response.ToNodes(forceSsl); + return Tuple.Create>(response, + new ReadOnlyCollection(nodes.ToArray())); + } + + /// + public override RequestData CreatePingRequestData(Node node, RequestConfiguration requestConfiguration, + ITransportConfiguration global, + MemoryStreamFactory memoryStreamFactory + ) + { + var requestParameters = new DefaultRequestParameters { - IRequestParameters requestParameters = new RequestParameters - { - RequestConfiguration = requestConfiguration - }; - - var data = new RequestData(HttpMethod.HEAD, string.Empty, null, global, requestParameters, - memoryStreamFactory) {Node = node}; - return data; - } - - /// - public async Task PingAsync(ITransportClient transportClient, RequestData pingData, - CancellationToken cancellationToken) => - await transportClient.RequestAsync(pingData, cancellationToken).ConfigureAwait(false); - - /// - public IApiCallDetails Ping(ITransportClient connection, RequestData pingData) => - connection.Request(pingData); + RequestConfiguration = requestConfiguration + }; + + var data = new RequestData(HttpMethod.HEAD, string.Empty, null, global, requestParameters, + memoryStreamFactory) {Node = node}; + return data; + } + + /// + public override async Task PingAsync(TransportClient transportClient, RequestData pingData, + CancellationToken cancellationToken) + { + var response = await transportClient.RequestAsync(pingData, cancellationToken).ConfigureAwait(false); + return response; + } + + /// + public override TransportResponse Ping(TransportClient connection, RequestData pingData) + { + var response = connection.Request(pingData); + return response; } } diff --git a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponse.cs b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponse.cs index 69a9e04..6cac0a2 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponse.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponse.cs @@ -7,118 +7,110 @@ using System.Text; using System.Text.Json.Serialization; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// +/// Base response for Elasticsearch responses. +/// +public abstract class ElasticsearchResponse : TransportResponse { /// - /// Base response for Elasticsearch responses. + /// A collection of warnings returned from Elasticsearch. + /// Used to provide server warnings, for example, when the request uses an API feature that is marked as deprecated. /// - public abstract class ElasticsearchResponse : IElasticsearchResponse + [JsonIgnore] + public IEnumerable ElasticsearchWarnings { - private IApiCallDetails? _originalApiCall; - - /// Returns useful information about the request(s) that were part of this API call. - [JsonIgnore] - public virtual IApiCallDetails? ApiCall => _originalApiCall; - - /// - /// A collection of warnings returned from Elasticsearch. - /// Used to provide server warnings, for example, when the request uses an API feature that is marked as deprecated. - /// - [JsonIgnore] - public IEnumerable Warnings + get { - get + if (ApiCallDetails.ParsedHeaders is not null && ApiCallDetails.ParsedHeaders.TryGetValue("warning", out var warnings)) { - if (ApiCall.ParsedHeaders is not null && ApiCall.ParsedHeaders.TryGetValue("warning", out var warnings)) - { - foreach (var warning in warnings) - yield return warning; - } + foreach (var warning in warnings) + yield return warning; } } + } - /// - [JsonIgnore] - public string DebugInformation + /// + /// + /// + /// + [JsonIgnore] + public string DebugInformation + { + get { - get + var sb = new StringBuilder(); + sb.Append($"{(!IsValid ? "Inv" : "V")}alid Elastic.Clients.Elasticsearch response built from a "); + sb.AppendLine(ApiCallDetails?.ToString().ToCamelCase() ?? + "null ApiCall which is highly exceptional, please open a bug if you see this"); + if (!IsValid) + DebugIsValid(sb); + + if (ApiCallDetails.ParsedHeaders is not null && ApiCallDetails.ParsedHeaders.TryGetValue("warning", out var warnings)) { - var sb = new StringBuilder(); - sb.Append($"{(!IsValid ? "Inv" : "V")}alid Elastic.Clients.Elasticsearch response built from a "); - sb.AppendLine(ApiCall?.ToString().ToCamelCase() ?? - "null ApiCall which is highly exceptional, please open a bug if you see this"); - if (!IsValid) - DebugIsValid(sb); - - if (ApiCall.ParsedHeaders is not null && ApiCall.ParsedHeaders.TryGetValue("warning", out var warnings)) - { - sb.AppendLine($"# Server indicated warnings:"); - - foreach (var warning in warnings) - sb.AppendLine($"- {warning}"); - } + sb.AppendLine($"# Server indicated warnings:"); - if (ApiCall != null) - ResponseStatics.DebugInformationBuilder(ApiCall, sb); - return sb.ToString(); + foreach (var warning in warnings) + sb.AppendLine($"- {warning}"); } + + if (ApiCallDetails != null) + Diagnostics.ResponseStatics.DebugInformationBuilder(ApiCallDetails, sb); + return sb.ToString(); } + } - /// - [JsonIgnore] - public virtual bool IsValid + /// + /// + /// + /// + [JsonIgnore] + public virtual bool IsValid + { + get { - get - { - var statusCode = ApiCall?.HttpStatusCode; + var statusCode = ApiCallDetails?.HttpStatusCode; - // TODO - Review this on a request by reqeust basis - if (statusCode == 404) - return false; + // TODO - Review this on a request by reqeust basis + if (statusCode == 404) + return false; - return (ApiCall?.Success ?? false) && (!ServerError?.HasError() ?? true); - } + return (ApiCallDetails?.Success ?? false) && (!ServerError?.HasError() ?? true); } + } - /// - [JsonIgnore] - public Exception? OriginalException => ApiCall?.OriginalException; + /// + /// + /// + [JsonIgnore] + public ElasticsearchServerError ServerError { get; internal set; } - IApiCallDetails? ITransportResponse.ApiCall + /// + /// + /// + /// + /// + // TODO: We need nullable annotations here ideally as exception is not null when the return value is true. + public bool TryGetOriginalException(out Exception? exception) + { + if (ApiCallDetails.OriginalException is not null) { - get => _originalApiCall; - set => _originalApiCall = value; + exception = ApiCallDetails.OriginalException; + return true; } - /// - /// - /// - [JsonIgnore] - public ServerError ServerError { get; internal set; } - - /// - /// - /// - /// - /// - // TODO: We need nullable annotations here ideally as exception is not null when the return value is true. - public bool TryGetOriginalException(out Exception? exception) - { - if (OriginalException is not null) - { - exception = OriginalException; - return true; - } - - exception = null; - return false; - } + exception = null; + return false; + } - /// Subclasses can override this to provide more information on why a call is not valid. - protected virtual void DebugIsValid(StringBuilder sb) { } + /// Subclasses can override this to provide more information on why a call is not valid. + protected virtual void DebugIsValid(StringBuilder sb) { } - /// - public override string ToString() => - $"{(!IsValid ? "Inv" : "V")}alid Elastic.Clients.Elasticsearch response built from a {ApiCall?.ToString().ToCamelCase()}"; - } + /// + /// + /// + /// + public override string ToString() => + $"{(!IsValid ? "Inv" : "V")}alid Elastic.Clients.Elasticsearch response built from a {ApiCallDetails?.ToString().ToCamelCase()}"; } diff --git a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponseBuilder.cs b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponseBuilder.cs index 0e95548..f155fb5 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponseBuilder.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/ElasticsearchResponseBuilder.cs @@ -6,43 +6,42 @@ using System.IO; using System.Text.Json; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +internal sealed class ElasticsearchResponseBuilder : DefaultResponseBuilder { - internal sealed class ElasticsearchResponseBuilder : DefaultResponseBuilder + protected override void SetErrorOnResponse(TResponse response, ElasticsearchServerError error) { - protected override void SetErrorOnResponse(TResponse response, ServerError error) + if (response is ElasticsearchResponse elasticResponse) { - if (response is ElasticsearchResponse elasticResponse) - { - elasticResponse.ServerError = error; - } + elasticResponse.ServerError = error; } + } + + protected override bool TryGetError(ApiCallDetails apiCallDetails, RequestData requestData, Stream responseStream, out ElasticsearchServerError error) + { + error = null; + + Debug.Assert(responseStream.CanSeek); + + var serializer = requestData.ConnectionSettings.RequestResponseSerializer; - protected override bool TryGetError(ApiCallDetails apiCallDetails, RequestData requestData, Stream responseStream, out ServerError error) + try { - error = null; - - Debug.Assert(responseStream.CanSeek); - - var serializer = requestData.ConnectionSettings.RequestResponseSerializer; - - try - { - error = serializer.Deserialize(responseStream); - return error is not null; - } - catch (JsonException) - { - // Empty catch as we'll try the original response type if the error serialization fails - } - finally - { - responseStream.Position = 0; - } - - return false; + error = serializer.Deserialize(responseStream); + return error is not null; + } + catch (JsonException) + { + // Empty catch as we'll try the original response type if the error serialization fails + } + finally + { + responseStream.Position = 0; } - protected sealed override bool RequiresErrorDeserialization(ApiCallDetails apiCallDetails, RequestData requestData) => apiCallDetails.HttpStatusCode > 399; + return false; } + + protected sealed override bool RequiresErrorDeserialization(ApiCallDetails apiCallDetails, RequestData requestData) => apiCallDetails.HttpStatusCode > 399; } diff --git a/src/Elastic.Transport/Products/Elasticsearch/Failures/ElasticsearchServerError.cs b/src/Elastic.Transport/Products/Elasticsearch/Failures/ElasticsearchServerError.cs new file mode 100644 index 0000000..3de79a8 --- /dev/null +++ b/src/Elastic.Transport/Products/Elasticsearch/Failures/ElasticsearchServerError.cs @@ -0,0 +1,83 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Transport.Products.Elasticsearch; + +/// Represents the error response as returned by Elasticsearch. +[DataContract] +public sealed class ElasticsearchServerError : ErrorResponse +{ + /// + public ElasticsearchServerError() { } + + /// + public ElasticsearchServerError(Error error, int? statusCode) + { + Error = error; + Status = statusCode.GetValueOrDefault(-1); + } + + /// an object that represents the server exception that occurred + [DataMember(Name = "error")] + [JsonPropertyName("error")] + public Error Error { get; init; } + + /// The HTTP status code returned from the server + [DataMember(Name = "status")] + [JsonPropertyName("status")] + public int Status { get; init; } = -1; + + /// + /// Try and create an instance of from + /// + /// Whether a an instance of was created successfully + public static bool TryCreate(Stream stream, out ElasticsearchServerError serverError) + { + try + { + serverError = Create(stream); + return serverError != null; + } + catch + { + serverError = null; + return false; + } + } + + /// + /// Use the clients default to create an instance + /// of from + /// + public static ElasticsearchServerError Create(Stream stream) => + LowLevelRequestResponseSerializer.Instance.Deserialize(stream); + + // ReSharper disable once UnusedMember.Global + /// + public static ValueTask CreateAsync(Stream stream, CancellationToken token = default) => + LowLevelRequestResponseSerializer.Instance.DeserializeAsync(stream, token); + + /// A human readable string representation of the server error returned by Elasticsearch + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append($"ServerError: {Status}"); + if (Error != null) + sb.Append(Error); + return sb.ToString(); + } + + /// + /// + /// + /// + public override bool HasError() => Status > 0 && Error is not null; +} diff --git a/src/Elastic.Transport/Products/Elasticsearch/Failures/Error.cs b/src/Elastic.Transport/Products/Elasticsearch/Failures/Error.cs index 0bfd6a7..49fc399 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/Failures/Error.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/Failures/Error.cs @@ -5,131 +5,129 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// Represents the serialized Elasticsearch java exception that caused a request to fail +[DataContract] +[JsonConverter(typeof(ErrorConverter))] +public sealed class Error : ErrorCause { - /// Represents the serialized Elasticsearch java exception that caused a request to fail - [DataContract] - [JsonConverter(typeof(ErrorConverter))] - public class Error : ErrorCause - { - private static readonly IReadOnlyDictionary DefaultHeaders = - new ReadOnlyDictionary(new Dictionary(0)); + private static readonly IReadOnlyDictionary DefaultHeaders = + new ReadOnlyDictionary(new Dictionary(0)); - /// Additional headers from the request that pertain to the error - [JsonPropertyName("headers")] - public IReadOnlyDictionary Headers { get; set; } = DefaultHeaders; + /// Additional headers from the request that pertain to the error + [JsonPropertyName("headers")] + public IReadOnlyDictionary Headers { get; set; } = DefaultHeaders; - /// The root cause exception - [JsonPropertyName("root_cause")] - public IReadOnlyCollection RootCause { get; set; } + /// The root cause exception + [JsonPropertyName("root_cause")] + public IReadOnlyCollection RootCause { get; set; } - /// A human readable string representation of the exception returned by Elasticsearch - public override string ToString() => CausedBy == null - ? $"Type: {Type} Reason: \"{Reason}\"" - : $"Type: {Type} Reason: \"{Reason}\" CausedBy: \"{CausedBy}\""; - } + /// A human readable string representation of the exception returned by Elasticsearch + public override string ToString() => CausedBy == null + ? $"Type: {Type} Reason: \"{Reason}\"" + : $"Type: {Type} Reason: \"{Reason}\" CausedBy: \"{CausedBy}\""; +} - internal sealed class ErrorConverter : JsonConverter +internal sealed class ErrorConverter : JsonConverter +{ + public override Error Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override Error Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.String) { - if (reader.TokenType == JsonTokenType.String) - { - return new Error { Reason = reader.GetString() }; - } - else if (reader.TokenType == JsonTokenType.StartObject) - { - var error = new Error(); - Dictionary additional = null; + return new Error { Reason = reader.GetString() }; + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + var error = new Error(); + Dictionary additional = null; - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) { - if (reader.TokenType == JsonTokenType.PropertyName) + if (reader.ValueTextEquals("root_cause")) { - if (reader.ValueTextEquals("root_cause")) - { - var value = JsonSerializer.Deserialize>(ref reader, options); - error.RootCause = value; - continue; - } - - if (reader.ValueTextEquals("caused_by")) - { - var value = JsonSerializer.Deserialize(ref reader, options); - error.CausedBy = value; - continue; - } - - //if (reader.ValueTextEquals("suppressed")) - //{ - // var value = JsonSerializer.Deserialize>(ref reader, options); - // error.TODO = value; - // continue; - //} - - if (reader.ValueTextEquals("headers")) - { - var value = JsonSerializer.Deserialize>(ref reader, options); - error.Headers = value; - continue; - } - - if (reader.ValueTextEquals("stack_trace")) - { - var value = JsonSerializer.Deserialize(ref reader, options); - error.StackTrace = value; - continue; - } - - if (reader.ValueTextEquals("type")) - { - var value = JsonSerializer.Deserialize(ref reader, options); - error.Type = value; - continue; - } - - if (reader.ValueTextEquals("index")) - { - var value = JsonSerializer.Deserialize(ref reader, options); - error.Index = value; - continue; - } - - if (reader.ValueTextEquals("index_uuid")) - { - var value = JsonSerializer.Deserialize(ref reader, options); - error.IndexUUID = value; - continue; - } - - if (reader.ValueTextEquals("reason")) - { - var value = JsonSerializer.Deserialize(ref reader, options); - error.Reason = value; - continue; - } - - additional ??= new Dictionary(); - var key = reader.GetString(); - var additionaValue = JsonSerializer.Deserialize(ref reader, options); - additional.Add(key, additionaValue); + var value = JsonSerializer.Deserialize>(ref reader, options); + error.RootCause = value; + continue; + } + + if (reader.ValueTextEquals("caused_by")) + { + var value = JsonSerializer.Deserialize(ref reader, options); + error.CausedBy = value; + continue; + } + + //if (reader.ValueTextEquals("suppressed")) + //{ + // var value = JsonSerializer.Deserialize>(ref reader, options); + // error.TODO = value; + // continue; + //} + + if (reader.ValueTextEquals("headers")) + { + var value = JsonSerializer.Deserialize>(ref reader, options); + error.Headers = value; + continue; + } + + if (reader.ValueTextEquals("stack_trace")) + { + var value = JsonSerializer.Deserialize(ref reader, options); + error.StackTrace = value; + continue; + } + + if (reader.ValueTextEquals("type")) + { + var value = JsonSerializer.Deserialize(ref reader, options); + error.Type = value; + continue; + } + + if (reader.ValueTextEquals("index")) + { + var value = JsonSerializer.Deserialize(ref reader, options); + error.Index = value; + continue; } - } - if (additional is not null) - error.AdditionalProperties = additional; + if (reader.ValueTextEquals("index_uuid")) + { + var value = JsonSerializer.Deserialize(ref reader, options); + error.IndexUUID = value; + continue; + } - return error; + if (reader.ValueTextEquals("reason")) + { + var value = JsonSerializer.Deserialize(ref reader, options); + error.Reason = value; + continue; + } + + additional ??= new Dictionary(); + var key = reader.GetString(); + var additionaValue = JsonSerializer.Deserialize(ref reader, options); + additional.Add(key, additionaValue); + } } - throw new JsonException("Could not deserialise the error response."); + if (additional is not null) + error.AdditionalProperties = additional; + + return error; } - public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options) => throw new NotImplementedException(); + throw new JsonException("Could not deserialise the error response."); } + + public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options) => throw new NotImplementedException(); } diff --git a/src/Elastic.Transport/Products/Elasticsearch/Failures/ErrorCause.cs b/src/Elastic.Transport/Products/Elasticsearch/Failures/ErrorCause.cs index 7d9eda3..89c07a9 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/Failures/ErrorCause.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/Failures/ErrorCause.cs @@ -7,85 +7,84 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// Represents an Elasticsearch server exception. +[DataContract] +[JsonConverter(typeof(ErrorCauseConverter))] +public class ErrorCause { - /// Represents an Elasticsearch server exception. - [DataContract] - [JsonConverter(typeof(ErrorCauseConverter))] - public class ErrorCause - { - //private static readonly IReadOnlyCollection DefaultCollection = - // new ReadOnlyCollection(new string[0]); - - private static readonly IReadOnlyDictionary DefaultDictionary = - new ReadOnlyDictionary(new Dictionary()); - - //private static readonly IReadOnlyCollection DefaultFailedShards = - // new ReadOnlyCollection(new ShardFailure[0]); - - /// - /// Additional properties related to the error cause. Contains properties that - /// are not explicitly mapped on - /// - public IReadOnlyDictionary AdditionalProperties { get; set; } = DefaultDictionary; - - /// The name of the Elasticsearch server exception that was thrown - public string Type { get; set; } - - /// - /// If stacktrace was requested this holds the java stack trace as it occurred on the server - /// - [JsonPropertyName("stack_trace")] - public string StackTrace { get; set; } - - /// - /// The exception message of the exception that was thrown on the server causing the request to fail - /// - public string Reason { get; set; } + //private static readonly IReadOnlyCollection DefaultCollection = + // new ReadOnlyCollection(new string[0]); + + private static readonly IReadOnlyDictionary DefaultDictionary = + new ReadOnlyDictionary(new Dictionary()); + + //private static readonly IReadOnlyCollection DefaultFailedShards = + // new ReadOnlyCollection(new ShardFailure[0]); + + /// + /// Additional properties related to the error cause. Contains properties that + /// are not explicitly mapped on + /// + public IReadOnlyDictionary AdditionalProperties { get; internal set; } = DefaultDictionary; + + /// The name of the Elasticsearch server exception that was thrown + public string Type { get; internal set; } + + /// + /// If stacktrace was requested this holds the java stack trace as it occurred on the server + /// + [JsonPropertyName("stack_trace")] + public string StackTrace { get; internal set; } + + /// + /// The exception message of the exception that was thrown on the server causing the request to fail + /// + public string Reason { get; internal set; } // The following are all very specific to individual failures // Seeking to clean this up within Elasticsearch itself: https://github.com/elastic/elasticsearch/issues/27672 #pragma warning disable 1591 - //public long? BytesLimit { get; set; } + //public long? BytesLimit { get; set; } - //public long? BytesWanted { get; set; } + //public long? BytesWanted { get; set; } - public ErrorCause CausedBy { get; set; } + public ErrorCause CausedBy { get; internal set; } - //public int? Column { get; set; } + //public int? Column { get; set; } - //public IReadOnlyCollection FailedShards { get; set; } = DefaultFailedShards; + //public IReadOnlyCollection FailedShards { get; set; } = DefaultFailedShards; - //public bool? Grouped { get; set; } + //public bool? Grouped { get; set; } - public string Index { get; set; } + public string Index { get; internal set; } - public string IndexUUID { get; set; } + public string IndexUUID { get; internal set; } - //public string Language { get; set; } + //public string Language { get; set; } - //public string LicensedExpiredFeature { get; set; } + //public string LicensedExpiredFeature { get; set; } - //public int? Line { get; set; } + //public int? Line { get; set; } - //public string Phase { get; set; } + //public string Phase { get; set; } - //public IReadOnlyCollection ResourceId { get; set; } = DefaultCollection; + //public IReadOnlyCollection ResourceId { get; set; } = DefaultCollection; - //public string ResourceType { get; set; } + //public string ResourceType { get; set; } - //public string Script { get; set; } + //public string Script { get; set; } - //public IReadOnlyCollection ScriptStack { get; set; } = DefaultCollection; + //public IReadOnlyCollection ScriptStack { get; set; } = DefaultCollection; - // TODO: This attribute is supported from 5.8 onward. At the moment Transport depends on that version but is that safe? - //[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] - //public int? Shard { get; set; } + // TODO: This attribute is supported from 5.8 onward. At the moment Transport depends on that version but is that safe? + //[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + //public int? Shard { get; set; } #pragma warning restore 1591 - /// A human readable string representation of the exception returned by Elasticsearch - public override string ToString() => CausedBy == null - ? $"Type: {Type} Reason: \"{Reason}\"" - : $"Type: {Type} Reason: \"{Reason}\" CausedBy: \"{CausedBy}\""; - } + /// A human readable string representation of the exception returned by Elasticsearch + public override string ToString() => CausedBy == null + ? $"Type: {Type} Reason: \"{Reason}\"" + : $"Type: {Type} Reason: \"{Reason}\" CausedBy: \"{CausedBy}\""; } diff --git a/src/Elastic.Transport/Products/Elasticsearch/Failures/ServerError.cs b/src/Elastic.Transport/Products/Elasticsearch/Failures/ServerError.cs deleted file mode 100644 index 721a805..0000000 --- a/src/Elastic.Transport/Products/Elasticsearch/Failures/ServerError.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.IO; -using System.Runtime.Serialization; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; - -namespace Elastic.Transport.Products.Elasticsearch -{ - /// Represents the error response as returned by Elasticsearch. - [DataContract] - public class ServerError : ErrorResponse - { - /// - public ServerError() { } - - /// - public ServerError(Error error, int? statusCode) - { - Error = error; - Status = statusCode.GetValueOrDefault(-1); - } - - /// an object that represents the server exception that occurred - [DataMember(Name = "error")] - [JsonPropertyName("error")] - public Error Error { get; set; } - - /// The HTTP status code returned from the server - [DataMember(Name = "status")] - [JsonPropertyName("status")] - public int Status { get; set; } = -1; - - /// - /// Try and create an instance of from - /// - /// Whether a an instance of was created successfully - public static bool TryCreate(Stream stream, out ServerError serverError) - { - try - { - serverError = Create(stream); - return serverError != null; - } - catch - { - serverError = null; - return false; - } - } - - /// - /// Use the clients default to create an instance - /// of from - /// - public static ServerError Create(Stream stream) => - LowLevelRequestResponseSerializer.Instance.Deserialize(stream); - - // ReSharper disable once UnusedMember.Global - /// - public static ValueTask CreateAsync(Stream stream, CancellationToken token = default) => - LowLevelRequestResponseSerializer.Instance.DeserializeAsync(stream, token); - - /// A human readable string representation of the server error returned by Elasticsearch - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append($"ServerError: {Status}"); - if (Error != null) - sb.Append(Error); - return sb.ToString(); - } - - /// - /// - /// - /// - public override bool HasError() => Status > 0 && Error is not null; - } -} diff --git a/src/Elastic.Transport/Products/Elasticsearch/Failures/ShardFailure.cs b/src/Elastic.Transport/Products/Elasticsearch/Failures/ShardFailure.cs index 5722954..9f4b39b 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/Failures/ShardFailure.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/Failures/ShardFailure.cs @@ -5,33 +5,32 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Elastic.Transport.Products.Elasticsearch +namespace Elastic.Transport.Products.Elasticsearch; + +/// Represents a failure that occurred on a shard involved in the request +[DataContract] +public sealed class ShardFailure { - /// Represents a failure that occurred on a shard involved in the request - [DataContract] - public class ShardFailure - { - /// This index this shard belongs to - [JsonPropertyName("index")] - public string Index { get; set; } + /// This index this shard belongs to + [JsonPropertyName("index")] + public string Index { get; set; } - /// The node the shard is currently allocated on - [JsonPropertyName("node")] - public string Node { get; set; } + /// The node the shard is currently allocated on + [JsonPropertyName("node")] + public string Node { get; set; } - /// - /// The java exception that caused the shard to fail - /// - [JsonPropertyName("reason")] - public ErrorCause Reason { get; set; } + /// + /// The java exception that caused the shard to fail + /// + [JsonPropertyName("reason")] + public ErrorCause Reason { get; set; } - /// The shard number that failed - [JsonPropertyName("shard")] - [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] - public int? Shard { get; set; } + /// The shard number that failed + [JsonPropertyName("shard")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public int? Shard { get; set; } - /// The status of the shard when the exception occured - [JsonPropertyName("status")] - public string Status { get; set; } - } + /// The status of the shard when the exception occured + [JsonPropertyName("status")] + public string Status { get; set; } } diff --git a/src/Elastic.Transport/Products/Elasticsearch/IElasticsearchResponse.cs b/src/Elastic.Transport/Products/Elasticsearch/IElasticsearchResponse.cs deleted file mode 100644 index 74fd9a5..0000000 --- a/src/Elastic.Transport/Products/Elasticsearch/IElasticsearchResponse.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Text.Json.Serialization; - -namespace Elastic.Transport.Products.Elasticsearch -{ - /// - /// A response from Elasticsearch - /// - public interface IElasticsearchResponse : ITransportResponse - { - /// - /// A lazily computed, human readable string representation of what happened during a request for both successful and - /// failed requests. Useful whilst developing or to log when is false on responses. - /// - [JsonIgnore] - string DebugInformation { get; } - - /// - /// Checks if a response is functionally valid or not. - /// This is a Elastic.Clients.Elasticsearch abstraction to have a single property to check whether there was something wrong with a request. - /// - /// For instance, an Elasticsearch bulk response always returns 200 and individual bulk items may fail, - /// will be false in that case. - /// - /// - /// You can also configure the client to always throw an using - /// if the response is not valid - /// - /// - [JsonIgnore] - bool IsValid { get; } - - /// - /// If the request resulted in an exception on the client side this will hold the exception that was thrown. - /// - /// This property is a shortcut to 's - /// and - /// is possibly set when is false depending on the cause of the error - /// - /// - /// You can also configure the client to always throw an using - /// if the response is not valid - /// - /// - [JsonIgnore] - Exception? OriginalException { get; } - } -} diff --git a/src/Elastic.Transport/Products/Elasticsearch/Sniff/NodeInfo.cs b/src/Elastic.Transport/Products/Elasticsearch/Sniff/NodeInfo.cs new file mode 100644 index 0000000..d795a04 --- /dev/null +++ b/src/Elastic.Transport/Products/Elasticsearch/Sniff/NodeInfo.cs @@ -0,0 +1,42 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Elastic.Transport.Products.Elasticsearch; + +internal sealed class NodeInfo +{ + public string build_hash { get; set; } + public string host { get; set; } + public NodeInfoHttp http { get; set; } + public string ip { get; set; } + public string name { get; set; } + public IList roles { get; set; } + public IDictionary settings { get; set; } + public string transport_address { get; set; } + public string version { get; set; } + internal bool HoldsData => roles?.Contains("data") ?? false; + + internal bool HttpEnabled + { + get + { + if (settings != null && settings.TryGetValue("http.enabled", out var httpEnabled)) + { + if (httpEnabled is JsonElement e) + return e.GetBoolean(); + return Convert.ToBoolean(httpEnabled); + } + + return http != null; + } + } + + internal bool IngestEnabled => roles?.Contains("ingest") ?? false; + + internal bool MasterEligible => roles?.Contains("master") ?? false; +} diff --git a/src/Elastic.Transport/Products/Elasticsearch/Sniff/NodeInfoHttp.cs b/src/Elastic.Transport/Products/Elasticsearch/Sniff/NodeInfoHttp.cs new file mode 100644 index 0000000..3c8519a --- /dev/null +++ b/src/Elastic.Transport/Products/Elasticsearch/Sniff/NodeInfoHttp.cs @@ -0,0 +1,13 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections.Generic; + +namespace Elastic.Transport.Products.Elasticsearch; + +internal sealed class NodeInfoHttp +{ + public IList bound_address { get; set; } + public string publish_address { get; set; } +} diff --git a/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffParser.cs b/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffParser.cs new file mode 100644 index 0000000..268b245 --- /dev/null +++ b/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffParser.cs @@ -0,0 +1,42 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Text.RegularExpressions; +using Elastic.Transport.Extensions; + +namespace Elastic.Transport.Products.Elasticsearch; + +/// +/// Elasticsearch returns addresses in the form of +/// [fqdn]/ip:port number +/// This helper parses it to +/// +public static class SniffParser +{ + /// A regular expression that captures fqdn, ip and por + public static Regex AddressRegex { get; } = + new Regex(@"^((?[^/]+)/)?(?[^:]+|\[[\da-fA-F:\.]+\]):(?\d+)$"); + + /// + /// Elasticsearch returns addresses in the form of + /// [fqdn]/ip:port number + /// This helper parses it to + /// + public static Uri ParseToUri(string boundAddress, bool forceHttp) + { + if (boundAddress == null) throw new ArgumentNullException(nameof(boundAddress)); + + var suffix = forceHttp ? "s" : string.Empty; + var match = AddressRegex.Match(boundAddress); + if (!match.Success) throw new Exception($"Can not parse bound_address: {boundAddress} to Uri"); + + var fqdn = match.Groups["fqdn"].Value.Trim(); + var ip = match.Groups["ip"].Value.Trim(); + var port = match.Groups["port"].Value.Trim(); + var host = !fqdn.IsNullOrEmpty() ? fqdn : ip; + + return new Uri($"http{suffix}://{host}:{port}"); + } +} diff --git a/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffResponse.cs b/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffResponse.cs index 794c7e4..e5acae7 100644 --- a/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffResponse.cs +++ b/src/Elastic.Transport/Products/Elasticsearch/Sniff/SniffResponse.cs @@ -2,124 +2,47 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using Elastic.Transport.Extensions; using static Elastic.Transport.Products.Elasticsearch.ElasticsearchNodeFeatures; -namespace Elastic.Transport.Products.Elasticsearch -{ - /// - /// Elasticsearch returns addresses in the form of - /// [fqdn]/ip:port number - /// This helper parses it to - /// - public static class SniffParser - { - /// A regular expression that captures fqdn, ip and por - public static Regex AddressRegex { get; } = - new Regex(@"^((?[^/]+)/)?(?[^:]+|\[[\da-fA-F:\.]+\]):(?\d+)$"); - - /// - /// Elasticsearch returns addresses in the form of - /// [fqdn]/ip:port number - /// This helper parses it to - /// - public static Uri ParseToUri(string boundAddress, bool forceHttp) - { - if (boundAddress == null) throw new ArgumentNullException(nameof(boundAddress)); +namespace Elastic.Transport.Products.Elasticsearch; - var suffix = forceHttp ? "s" : string.Empty; - var match = AddressRegex.Match(boundAddress); - if (!match.Success) throw new Exception($"Can not parse bound_address: {boundAddress} to Uri"); - - var fqdn = match.Groups["fqdn"].Value.Trim(); - var ip = match.Groups["ip"].Value.Trim(); - var port = match.Groups["port"].Value.Trim(); - var host = !fqdn.IsNullOrEmpty() ? fqdn : ip; - - return new Uri($"http{suffix}://{host}:{port}"); - } - } - - internal class SniffResponse : TransportResponse - { - // ReSharper disable InconsistentNaming - public string cluster_name { get; set; } - - public Dictionary nodes { get; set; } - - public IEnumerable ToNodes(bool forceHttp = false) - { - foreach (var kv in nodes.Where(n => n.Value.HttpEnabled)) - { - var info = kv.Value; - var httpEndpoint = info.http?.publish_address; - if (string.IsNullOrWhiteSpace(httpEndpoint)) - httpEndpoint = kv.Value.http?.bound_address.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(httpEndpoint)) - continue; - - var uri = SniffParser.ParseToUri(httpEndpoint, forceHttp); - var features = new List(); - if (info.MasterEligible) - features.Add(MasterEligible); - if (info.HoldsData) - features.Add(HoldsData); - if (info.IngestEnabled) - features.Add(IngestEnabled); - if (info.HttpEnabled) - features.Add(HttpEnabled); +internal sealed class SniffResponse : TransportResponse +{ + // ReSharper disable InconsistentNaming + public string cluster_name { get; set; } - var node = new Node(uri, features) - { - Name = info.name, Id = kv.Key, Settings = new ReadOnlyDictionary(info.settings) - }; - yield return node; - } - } - } + public Dictionary nodes { get; set; } - internal class NodeInfo + public IEnumerable ToNodes(bool forceHttp = false) { - public string build_hash { get; set; } - public string host { get; set; } - public NodeInfoHttp http { get; set; } - public string ip { get; set; } - public string name { get; set; } - public IList roles { get; set; } - public IDictionary settings { get; set; } - public string transport_address { get; set; } - public string version { get; set; } - internal bool HoldsData => roles?.Contains("data") ?? false; - - internal bool HttpEnabled + foreach (var kv in nodes.Where(n => n.Value.HttpEnabled)) { - get + var info = kv.Value; + var httpEndpoint = info.http?.publish_address; + if (string.IsNullOrWhiteSpace(httpEndpoint)) + httpEndpoint = kv.Value.http?.bound_address.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(httpEndpoint)) + continue; + + var uri = SniffParser.ParseToUri(httpEndpoint, forceHttp); + var features = new List(); + if (info.MasterEligible) + features.Add(MasterEligible); + if (info.HoldsData) + features.Add(HoldsData); + if (info.IngestEnabled) + features.Add(IngestEnabled); + if (info.HttpEnabled) + features.Add(HttpEnabled); + + var node = new Node(uri, features) { - if (settings != null && settings.TryGetValue("http.enabled", out var httpEnabled)) - { - if (httpEnabled is JsonElement e) - return e.GetBoolean(); - return Convert.ToBoolean(httpEnabled); - } - - return http != null; - } + Name = info.name, Id = kv.Key, Settings = new ReadOnlyDictionary(info.settings) + }; + yield return node; } - - internal bool IngestEnabled => roles?.Contains("ingest") ?? false; - - internal bool MasterEligible => roles?.Contains("master") ?? false; - } - - internal class NodeInfoHttp - { - public IList bound_address { get; set; } - public string publish_address { get; set; } } } diff --git a/src/Elastic.Transport/Products/IProductRegistration.cs b/src/Elastic.Transport/Products/IProductRegistration.cs deleted file mode 100644 index c33386b..0000000 --- a/src/Elastic.Transport/Products/IProductRegistration.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Elastic.Transport.Products -{ - /// - /// When interfaces with a product some parts are - /// bespoke for each product. This interface defines the contract products will have to implement in order to fill - /// in these bespoke parts. - /// The expectation is that unless you instantiate - /// directly clients that utilize transport will fill in this dependency - /// - /// - /// If you do want to use a bare-bones you can use - /// - /// - /// - public interface IProductRegistration - { - /// - /// The name of the current product utilizing - /// This name makes its way into the transport diagnostics sources and the default user agent string - /// - string Name { get; } - - /// - /// Whether the product will call out to supports ping endpoints - /// - bool SupportsPing { get; } - - /// - /// Whether the product will call out to supports sniff endpoints that return - /// information about available nodes - /// - bool SupportsSniff { get; } - - /// - /// The set of headers to parse from all requests by default. These can be added to any consumer specific requirements. - /// - HeadersList ResponseHeadersToParse { get; } - - /// - /// Create an instance of that describes where and how to ping see - /// All the parameters of this method correspond with 's constructor - /// - RequestData CreatePingRequestData(Node node, RequestConfiguration requestConfiguration, ITransportConfiguration global, IMemoryStreamFactory memoryStreamFactory); - - /// - /// Provide an implementation that performs the ping directly using and the - /// return by - /// - Task PingAsync(ITransportClient connection, RequestData pingData, CancellationToken cancellationToken); - - /// - /// Provide an implementation that performs the ping directly using and the - /// return by - /// - IApiCallDetails Ping(ITransportClient connection, RequestData pingData); - - /// - /// Create an instance of that describes where and how to sniff the cluster using - /// All the parameters of this method correspond with 's constructor - /// - RequestData CreateSniffRequestData(Node node, IRequestConfiguration requestConfiguration, ITransportConfiguration settings, - IMemoryStreamFactory memoryStreamFactory - ); - - /// - /// Provide an implementation that performs the sniff directly using and the - /// return by - /// - Task>> SniffAsync(ITransportClient connection, bool forceSsl, RequestData requestData, CancellationToken cancellationToken); - - /// - /// Provide an implementation that performs the sniff directly using and the - /// return by - /// - Tuple> Sniff(ITransportClient connection, bool forceSsl, RequestData requestData); - - /// Allows certain nodes to be queried first to obtain sniffing information - int SniffOrder(Node node); - - /// Predicate indicating a node is allowed to be used for API calls - /// The node to inspect - /// bool, true if node should allows API calls - bool NodePredicate(Node node); - - /// - /// Used by to determine if it needs to return true or false for - /// - /// - bool HttpStatusCodeClassifier(HttpMethod method, int statusCode); - - /// Try to obtain a server error from the response, this is used for debugging and exception messages - bool TryGetServerErrorReason(TResponse response, out string reason) where TResponse : ITransportResponse; - - /// - /// TODO - /// - MetaHeaderProvider MetaHeaderProvider { get; } - - /// - /// TODO - /// - ResponseBuilder ResponseBuilder { get; } - } -} diff --git a/src/Elastic.Transport/Products/ProductRegistration.cs b/src/Elastic.Transport/Products/ProductRegistration.cs new file mode 100644 index 0000000..27e397f --- /dev/null +++ b/src/Elastic.Transport/Products/ProductRegistration.cs @@ -0,0 +1,111 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Elastic.Transport.Products; + +/// +/// When interfaces with a product some parts are +/// bespoke for each product. This interface defines the contract products will have to implement in order to fill +/// in these bespoke parts. +/// The expectation is that unless you instantiate +/// directly clients that utilize transport will fill in this dependency +/// +/// +/// If you do want to use a bare-bones you can use +/// +/// +/// +public abstract class ProductRegistration +{ + /// + /// The name of the current product utilizing + /// This name makes its way into the transport diagnostics sources and the default user agent string + /// + public abstract string Name { get; } + + /// + /// Whether the product will call out to supports ping endpoints + /// + public abstract bool SupportsPing { get; } + + /// + /// Whether the product will call out to supports sniff endpoints that return + /// information about available nodes + /// + public abstract bool SupportsSniff { get; } + + /// + /// The set of headers to parse from all requests by default. These can be added to any consumer specific requirements. + /// + public abstract HeadersList ResponseHeadersToParse { get; } + + /// + /// Create an instance of that describes where and how to ping see + /// All the parameters of this method correspond with 's constructor + /// + public abstract RequestData CreatePingRequestData(Node node, RequestConfiguration requestConfiguration, ITransportConfiguration global, MemoryStreamFactory memoryStreamFactory); + + /// + /// Provide an implementation that performs the ping directly using and the + /// return by + /// + public abstract Task PingAsync(TransportClient connection, RequestData pingData, CancellationToken cancellationToken); + + /// + /// Provide an implementation that performs the ping directly using and the + /// return by + /// + public abstract TransportResponse Ping(TransportClient connection, RequestData pingData); + + /// + /// Create an instance of that describes where and how to sniff the cluster using + /// All the parameters of this method correspond with 's constructor + /// + public abstract RequestData CreateSniffRequestData(Node node, IRequestConfiguration requestConfiguration, ITransportConfiguration settings, + MemoryStreamFactory memoryStreamFactory); + + /// + /// Provide an implementation that performs the sniff directly using and the + /// return by + /// + public abstract Task>> SniffAsync(TransportClient connection, bool forceSsl, RequestData requestData, CancellationToken cancellationToken); + + /// + /// Provide an implementation that performs the sniff directly using and the + /// return by + /// + public abstract Tuple> Sniff(TransportClient connection, bool forceSsl, RequestData requestData); + + /// Allows certain nodes to be queried first to obtain sniffing information + public abstract int SniffOrder(Node node); + + /// Predicate indicating a node is allowed to be used for API calls + /// The node to inspect + /// bool, true if node should allows API calls + public abstract bool NodePredicate(Node node); + + /// + /// Used by to determine if it needs to return true or false for + /// + /// + public abstract bool HttpStatusCodeClassifier(HttpMethod method, int statusCode); + + /// Try to obtain a server error from the response, this is used for debugging and exception messages + public abstract bool TryGetServerErrorReason(TResponse response, out string reason) where TResponse : TransportResponse; + + /// + /// TODO + /// + public abstract MetaHeaderProvider MetaHeaderProvider { get; } + + /// + /// TODO + /// + public abstract ResponseBuilder ResponseBuilder { get; } +} diff --git a/src/Elastic.Transport/Requests/Body/PostData.ByteArray.cs b/src/Elastic.Transport/Requests/Body/PostData.ByteArray.cs index 6493d20..9843d80 100644 --- a/src/Elastic.Transport/Requests/Body/PostData.ByteArray.cs +++ b/src/Elastic.Transport/Requests/Body/PostData.ByteArray.cs @@ -6,53 +6,52 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +public abstract partial class PostData { - public abstract partial class PostData - { - /// - /// Create a instance that will write to the output - /// - // ReSharper disable once MemberCanBePrivate.Global - public static PostData Bytes(byte[] bytes) => new PostDataByteArray(bytes); + /// + /// Create a instance that will write to the output + /// + // ReSharper disable once MemberCanBePrivate.Global + public static PostData Bytes(byte[] bytes) => new PostDataByteArray(bytes); - private class PostDataByteArray : PostData + private class PostDataByteArray : PostData + { + protected internal PostDataByteArray(byte[] item) { - protected internal PostDataByteArray(byte[] item) - { - WrittenBytes = item; - Type = PostType.ByteArray; - } + WrittenBytes = item; + Type = PostType.ByteArray; + } - public override void Write(Stream writableStream, ITransportConfiguration settings) - { - if (WrittenBytes == null) return; + public override void Write(Stream writableStream, ITransportConfiguration settings) + { + if (WrittenBytes == null) return; - var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); + var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); - if (!disableDirectStreaming) - stream.Write(WrittenBytes, 0, WrittenBytes.Length); - else - buffer = settings.MemoryStreamFactory.Create(WrittenBytes); + if (!disableDirectStreaming) + stream.Write(WrittenBytes, 0, WrittenBytes.Length); + else + buffer = settings.MemoryStreamFactory.Create(WrittenBytes); - FinishStream(writableStream, buffer, settings); - } + FinishStream(writableStream, buffer, settings); + } - public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, - CancellationToken cancellationToken) - { - if (WrittenBytes == null) return; + public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, + CancellationToken cancellationToken) + { + if (WrittenBytes == null) return; - var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); + var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); - if (!disableDirectStreaming) - await stream.WriteAsync(WrittenBytes, 0, WrittenBytes.Length, cancellationToken) - .ConfigureAwait(false); - else - buffer = settings.MemoryStreamFactory.Create(WrittenBytes); + if (!disableDirectStreaming) + await stream.WriteAsync(WrittenBytes, 0, WrittenBytes.Length, cancellationToken) + .ConfigureAwait(false); + else + buffer = settings.MemoryStreamFactory.Create(WrittenBytes); - await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); - } + await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Elastic.Transport/Requests/Body/PostData.MultiJson.cs b/src/Elastic.Transport/Requests/Body/PostData.MultiJson.cs index ea07004..6dd3341 100644 --- a/src/Elastic.Transport/Requests/Body/PostData.MultiJson.cs +++ b/src/Elastic.Transport/Requests/Body/PostData.MultiJson.cs @@ -9,149 +9,148 @@ using System.Threading.Tasks; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +public abstract partial class PostData { - public abstract partial class PostData + /// + /// Create a instance that will write the as multiline/ndjson. + /// + public static PostData MultiJson(IEnumerable listOfString) => + new PostDataMultiJson(listOfString); + + /// + /// Create a instance that will serialize the as multiline/ndjson. + /// + public static PostData MultiJson(IEnumerable listOfSerializables) => + new PostDataMultiJson(listOfSerializables); + + private class PostDataMultiJson : PostData { - /// - /// Create a instance that will write the as multiline/ndjson. - /// - public static PostData MultiJson(IEnumerable listOfString) => - new PostDataMultiJson(listOfString); - - /// - /// Create a instance that will serialize the as multiline/ndjson. - /// - public static PostData MultiJson(IEnumerable listOfSerializables) => - new PostDataMultiJson(listOfSerializables); - - private class PostDataMultiJson : PostData + private readonly IEnumerable _enumerableOfObject; + private readonly IEnumerable _enumerableOfStrings; + + protected internal PostDataMultiJson(IEnumerable item) { - private readonly IEnumerable _enumerableOfObject; - private readonly IEnumerable _enumerableOfStrings; + _enumerableOfStrings = item; + Type = PostType.EnumerableOfString; + } - protected internal PostDataMultiJson(IEnumerable item) - { - _enumerableOfStrings = item; - Type = PostType.EnumerableOfString; - } + protected internal PostDataMultiJson(IEnumerable item) + { + _enumerableOfObject = item; + Type = PostType.EnumerableOfObject; + } - protected internal PostDataMultiJson(IEnumerable item) - { - _enumerableOfObject = item; - Type = PostType.EnumerableOfObject; - } + public override void Write(Stream writableStream, ITransportConfiguration settings) + { + if (Type != PostType.EnumerableOfObject && Type != PostType.EnumerableOfString) + throw new Exception( + $"{nameof(PostDataMultiJson)} only does not support {nameof(PostType)}.{Type.GetStringValue()}"); + + var stream = InitWrite(writableStream, settings, out var buffer, out _); - public override void Write(Stream writableStream, ITransportConfiguration settings) + switch (Type) { - if (Type != PostType.EnumerableOfObject && Type != PostType.EnumerableOfString) - throw new Exception( - $"{nameof(PostDataMultiJson)} only does not support {nameof(PostType)}.{Type.GetStringValue()}"); + case PostType.EnumerableOfString: + { + if (_enumerableOfStrings == null) return; - var stream = InitWrite(writableStream, settings, out var buffer, out _); + using var enumerator = _enumerableOfStrings.GetEnumerator(); + if (!enumerator.MoveNext()) + return; - switch (Type) - { - case PostType.EnumerableOfString: + BufferIfNeeded(settings, ref buffer, ref stream); + do { - if (_enumerableOfStrings == null) return; - - using var enumerator = _enumerableOfStrings.GetEnumerator(); - if (!enumerator.MoveNext()) - return; - - BufferIfNeeded(settings, ref buffer, ref stream); - do - { - var bytes = enumerator.Current.Utf8Bytes(); - stream.Write(bytes, 0, bytes.Length); - stream.Write(NewLineByteArray, 0, 1); - } while (enumerator.MoveNext()); - - break; - } - case PostType.EnumerableOfObject: - { - if (_enumerableOfObject == null) return; - - using var enumerator = _enumerableOfObject.GetEnumerator(); - if (!enumerator.MoveNext()) - return; - - BufferIfNeeded(settings, ref buffer, ref stream); - do - { - var o = enumerator.Current; - settings.RequestResponseSerializer.Serialize(o, stream, SerializationFormatting.None); - stream.Write(NewLineByteArray, 0, 1); - } while (enumerator.MoveNext()); - - break; - } - default: - throw new ArgumentOutOfRangeException(); + var bytes = enumerator.Current.Utf8Bytes(); + stream.Write(bytes, 0, bytes.Length); + stream.Write(NewLineByteArray, 0, 1); + } while (enumerator.MoveNext()); + + break; } + case PostType.EnumerableOfObject: + { + if (_enumerableOfObject == null) return; + + using var enumerator = _enumerableOfObject.GetEnumerator(); + if (!enumerator.MoveNext()) + return; - FinishStream(writableStream, buffer, settings); + BufferIfNeeded(settings, ref buffer, ref stream); + do + { + var o = enumerator.Current; + settings.RequestResponseSerializer.Serialize(o, stream, SerializationFormatting.None); + stream.Write(NewLineByteArray, 0, 1); + } while (enumerator.MoveNext()); + + break; + } + default: + throw new ArgumentOutOfRangeException(); } - public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, - CancellationToken cancellationToken) - { - if (Type != PostType.EnumerableOfObject && Type != PostType.EnumerableOfString) - throw new Exception( - $"{nameof(PostDataMultiJson)} only does not support {nameof(PostType)}.{Type.GetStringValue()}"); + FinishStream(writableStream, buffer, settings); + } - var stream = InitWrite(writableStream, settings, out var buffer, out _); + public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, + CancellationToken cancellationToken) + { + if (Type != PostType.EnumerableOfObject && Type != PostType.EnumerableOfString) + throw new Exception( + $"{nameof(PostDataMultiJson)} only does not support {nameof(PostType)}.{Type.GetStringValue()}"); + + var stream = InitWrite(writableStream, settings, out var buffer, out _); - switch (Type) + switch (Type) + { + case PostType.EnumerableOfString: { - case PostType.EnumerableOfString: - { - if (_enumerableOfStrings == null) - return; - - using var enumerator = _enumerableOfStrings.GetEnumerator(); - if (!enumerator.MoveNext()) - return; - - BufferIfNeeded(settings, ref buffer, ref stream); - do - { - var bytes = enumerator.Current.Utf8Bytes(); - await stream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); - await stream.WriteAsync(NewLineByteArray, 0, 1, cancellationToken).ConfigureAwait(false); - } while (enumerator.MoveNext()); - - break; - } - case PostType.EnumerableOfObject: + if (_enumerableOfStrings == null) + return; + + using var enumerator = _enumerableOfStrings.GetEnumerator(); + if (!enumerator.MoveNext()) + return; + + BufferIfNeeded(settings, ref buffer, ref stream); + do { - if (_enumerableOfObject == null) - return; - - using var enumerator = _enumerableOfObject.GetEnumerator(); - if (!enumerator.MoveNext()) - return; - - BufferIfNeeded(settings, ref buffer, ref stream); - do - { - var o = enumerator.Current; - await settings.RequestResponseSerializer.SerializeAsync(o, stream, - SerializationFormatting.None, cancellationToken) - .ConfigureAwait(false); - await stream.WriteAsync(NewLineByteArray, 0, 1, cancellationToken).ConfigureAwait(false); - } while (enumerator.MoveNext()); - - break; - } - default: - throw new ArgumentOutOfRangeException(); + var bytes = enumerator.Current.Utf8Bytes(); + await stream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(NewLineByteArray, 0, 1, cancellationToken).ConfigureAwait(false); + } while (enumerator.MoveNext()); + + break; } + case PostType.EnumerableOfObject: + { + if (_enumerableOfObject == null) + return; - await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); + using var enumerator = _enumerableOfObject.GetEnumerator(); + if (!enumerator.MoveNext()) + return; + + BufferIfNeeded(settings, ref buffer, ref stream); + do + { + var o = enumerator.Current; + await settings.RequestResponseSerializer.SerializeAsync(o, stream, + SerializationFormatting.None, cancellationToken) + .ConfigureAwait(false); + await stream.WriteAsync(NewLineByteArray, 0, 1, cancellationToken).ConfigureAwait(false); + } while (enumerator.MoveNext()); + + break; + } + default: + throw new ArgumentOutOfRangeException(); } + + await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Elastic.Transport/Requests/Body/PostData.Serializable.cs b/src/Elastic.Transport/Requests/Body/PostData.Serializable.cs index 3935040..5f30c42 100644 --- a/src/Elastic.Transport/Requests/Body/PostData.Serializable.cs +++ b/src/Elastic.Transport/Requests/Body/PostData.Serializable.cs @@ -7,55 +7,54 @@ using System.Threading.Tasks; using static Elastic.Transport.SerializationFormatting; -namespace Elastic.Transport +namespace Elastic.Transport; + +public abstract partial class PostData { - public abstract partial class PostData + /// + /// Create a instance that will serialize using + /// + /// + public static PostData Serializable(T data) => new SerializableData(data); + + private class SerializableData : PostData { - /// - /// Create a instance that will serialize using - /// - /// - public static PostData Serializable(T data) => new SerializableData(data); + private readonly T _serializable; - private class SerializableData : PostData + public SerializableData(T item) { - private readonly T _serializable; - - public SerializableData(T item) - { - Type = PostType.Serializable; - _serializable = item; - } - - public static implicit operator SerializableData(T serializableData) => - new SerializableData(serializableData); - - public override void Write(Stream writableStream, ITransportConfiguration settings) - { - MemoryStream buffer = null; - var stream = writableStream; - BufferIfNeeded(settings, ref buffer, ref stream); - - var indent = settings.PrettyJson ? Indented : None; - settings.RequestResponseSerializer.Serialize(_serializable, stream, indent); - - FinishStream(writableStream, buffer, settings); - } - - public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, - CancellationToken cancellationToken) - { - MemoryStream buffer = null; - var stream = writableStream; - BufferIfNeeded(settings, ref buffer, ref stream); - - var indent = settings.PrettyJson ? Indented : None; - await settings.RequestResponseSerializer - .SerializeAsync(_serializable, stream, indent, cancellationToken) - .ConfigureAwait(false); - - await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); - } + Type = PostType.Serializable; + _serializable = item; + } + + public static implicit operator SerializableData(T serializableData) => + new SerializableData(serializableData); + + public override void Write(Stream writableStream, ITransportConfiguration settings) + { + MemoryStream buffer = null; + var stream = writableStream; + BufferIfNeeded(settings, ref buffer, ref stream); + + var indent = settings.PrettyJson ? Indented : None; + settings.RequestResponseSerializer.Serialize(_serializable, stream, indent); + + FinishStream(writableStream, buffer, settings); + } + + public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, + CancellationToken cancellationToken) + { + MemoryStream buffer = null; + var stream = writableStream; + BufferIfNeeded(settings, ref buffer, ref stream); + + var indent = settings.PrettyJson ? Indented : None; + await settings.RequestResponseSerializer + .SerializeAsync(_serializable, stream, indent, cancellationToken) + .ConfigureAwait(false); + + await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Elastic.Transport/Requests/Body/PostData.Streamable.cs b/src/Elastic.Transport/Requests/Body/PostData.Streamable.cs index 4a36036..ab31659 100644 --- a/src/Elastic.Transport/Requests/Body/PostData.Streamable.cs +++ b/src/Elastic.Transport/Requests/Body/PostData.Streamable.cs @@ -7,63 +7,62 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +public abstract partial class PostData { - public abstract partial class PostData + /// + /// Create an instance of serializable data . This state is then passed to + /// and along with the to write to. Both will need to be supplied in order to + /// support both and + /// + /// The object we want to serialize later on + /// A func receiving and a to write to + /// A func receiving and a to write to + public static PostData StreamHandler(T state, Action syncWriter, + Func asyncWriter) => + new StreamableData(state, syncWriter, asyncWriter); + + /// + /// Represents an instance of that can handle . + /// Allows users full control over how they want to write data to the stream. + /// + /// The data or a state object used during writing, passed to the handlers to avoid boxing + private class StreamableData : PostData { - /// - /// Create an instance of serializable data . This state is then passed to - /// and along with the to write to. Both will need to be supplied in order to - /// support both and - /// - /// The object we want to serialize later on - /// A func receiving and a to write to - /// A func receiving and a to write to - public static PostData StreamHandler(T state, Action syncWriter, - Func asyncWriter) => - new StreamableData(state, syncWriter, asyncWriter); + private readonly T _state; + private readonly Action _syncWriter; + private readonly Func _asyncWriter; - /// - /// Represents an instance of that can handle . - /// Allows users full control over how they want to write data to the stream. - /// - /// The data or a state object used during writing, passed to the handlers to avoid boxing - private class StreamableData : PostData + public StreamableData(T state, Action syncWriter, + Func asyncWriter) { - private readonly T _state; - private readonly Action _syncWriter; - private readonly Func _asyncWriter; - - public StreamableData(T state, Action syncWriter, - Func asyncWriter) - { - _state = state; - const string message = "PostData.StreamHandler needs to handle both synchronous and async paths"; - _syncWriter = syncWriter ?? throw new ArgumentNullException(nameof(syncWriter), message); - _asyncWriter = asyncWriter ?? throw new ArgumentNullException(nameof(asyncWriter), message); - if (_syncWriter == null || _asyncWriter == null) - throw new ArgumentNullException(); - Type = PostType.StreamHandler; - } + _state = state; + const string message = "PostData.StreamHandler needs to handle both synchronous and async paths"; + _syncWriter = syncWriter ?? throw new ArgumentNullException(nameof(syncWriter), message); + _asyncWriter = asyncWriter ?? throw new ArgumentNullException(nameof(asyncWriter), message); + if (_syncWriter == null || _asyncWriter == null) + throw new ArgumentNullException(); + Type = PostType.StreamHandler; + } - public override void Write(Stream writableStream, ITransportConfiguration settings) - { - MemoryStream buffer = null; - var stream = writableStream; - BufferIfNeeded(settings, ref buffer, ref stream); - _syncWriter(_state, stream); - FinishStream(writableStream, buffer, settings); - } + public override void Write(Stream writableStream, ITransportConfiguration settings) + { + MemoryStream buffer = null; + var stream = writableStream; + BufferIfNeeded(settings, ref buffer, ref stream); + _syncWriter(_state, stream); + FinishStream(writableStream, buffer, settings); + } - public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, - CancellationToken cancellationToken) - { - MemoryStream buffer = null; - var stream = writableStream; - BufferIfNeeded(settings, ref buffer, ref stream); - await _asyncWriter(_state, stream, cancellationToken).ConfigureAwait(false); - await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); - } + public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, + CancellationToken cancellationToken) + { + MemoryStream buffer = null; + var stream = writableStream; + BufferIfNeeded(settings, ref buffer, ref stream); + await _asyncWriter(_state, stream, cancellationToken).ConfigureAwait(false); + await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Elastic.Transport/Requests/Body/PostData.String.cs b/src/Elastic.Transport/Requests/Body/PostData.String.cs index 4477512..a5c096a 100644 --- a/src/Elastic.Transport/Requests/Body/PostData.String.cs +++ b/src/Elastic.Transport/Requests/Body/PostData.String.cs @@ -7,65 +7,64 @@ using System.Threading.Tasks; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +public abstract partial class PostData { - public abstract partial class PostData - { - /// - /// Create a instance that will write to the output - /// - // ReSharper disable once MemberCanBePrivate.Global - public static PostData String(string serializedString) => new PostDataString(serializedString); + /// + /// Create a instance that will write to the output + /// + // ReSharper disable once MemberCanBePrivate.Global + public static PostData String(string serializedString) => new PostDataString(serializedString); - /// - /// string implicitly converts to so you do not have to use the static - /// factory method - /// - public static implicit operator PostData(string literalString) => String(literalString); + /// + /// string implicitly converts to so you do not have to use the static + /// factory method + /// + public static implicit operator PostData(string literalString) => String(literalString); - private class PostDataString : PostData - { - private readonly string _literalString; + private class PostDataString : PostData + { + private readonly string _literalString; - protected internal PostDataString(string item) - { - _literalString = item; - Type = PostType.LiteralString; - } + protected internal PostDataString(string item) + { + _literalString = item; + Type = PostType.LiteralString; + } - public override void Write(Stream writableStream, ITransportConfiguration settings) - { - if (string.IsNullOrEmpty(_literalString)) return; + public override void Write(Stream writableStream, ITransportConfiguration settings) + { + if (string.IsNullOrEmpty(_literalString)) return; - var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); + var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); - var stringBytes = WrittenBytes ?? _literalString.Utf8Bytes(); - WrittenBytes ??= stringBytes; - if (!disableDirectStreaming) - stream.Write(stringBytes, 0, stringBytes.Length); - else - buffer = settings.MemoryStreamFactory.Create(stringBytes); + var stringBytes = WrittenBytes ?? _literalString.Utf8Bytes(); + WrittenBytes ??= stringBytes; + if (!disableDirectStreaming) + stream.Write(stringBytes, 0, stringBytes.Length); + else + buffer = settings.MemoryStreamFactory.Create(stringBytes); - FinishStream(writableStream, buffer, settings); - } + FinishStream(writableStream, buffer, settings); + } - public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(_literalString)) return; + public override async Task WriteAsync(Stream writableStream, ITransportConfiguration settings, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(_literalString)) return; - var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); + var stream = InitWrite(writableStream, settings, out var buffer, out var disableDirectStreaming); - var stringBytes = WrittenBytes ?? _literalString.Utf8Bytes(); - WrittenBytes ??= stringBytes; - if (!disableDirectStreaming) - await stream.WriteAsync(stringBytes, 0, stringBytes.Length, cancellationToken) - .ConfigureAwait(false); - else - buffer = settings.MemoryStreamFactory.Create(stringBytes); + var stringBytes = WrittenBytes ?? _literalString.Utf8Bytes(); + WrittenBytes ??= stringBytes; + if (!disableDirectStreaming) + await stream.WriteAsync(stringBytes, 0, stringBytes.Length, cancellationToken) + .ConfigureAwait(false); + else + buffer = settings.MemoryStreamFactory.Create(stringBytes); - await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); - } + await FinishStreamAsync(writableStream, buffer, settings, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Elastic.Transport/Requests/Body/PostData.cs b/src/Elastic.Transport/Requests/Body/PostData.cs index 2e589f6..68f2c52 100644 --- a/src/Elastic.Transport/Requests/Body/PostData.cs +++ b/src/Elastic.Transport/Requests/Body/PostData.cs @@ -7,131 +7,130 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Represents the data the the user wishes to send over the wire. This abstract base class exposes +/// static factory methods to help you create wrap the various types of data we support. +/// +/// For raw bytes use +/// For raw string use +/// To serialize an object use +/// For use +/// To write your object directly to using a handler use +/// Multiline json is supported to using +/// and +/// +public abstract partial class PostData { /// - /// Represents the data the the user wishes to send over the wire. This abstract base class exposes - /// static factory methods to help you create wrap the various types of data we support. - /// - /// For raw bytes use - /// For raw string use - /// To serialize an object use - /// For use - /// To write your object directly to using a handler use - /// Multiline json is supported to using - /// and + /// The buffer size to use when calling /// - public abstract partial class PostData + // ReSharper disable once MemberCanBePrivate.Global + protected const int BufferSize = 81920; + + /// A static byte[] that hols a single new line feed + // ReSharper disable once MemberCanBePrivate.Global + protected static readonly byte[] NewLineByteArray = {(byte) '\n'}; + + /// + /// By setting this to true, and will buffer the data and + /// expose it on + /// + public bool? DisableDirectStreaming { get; set; } + + /// Reports the data this instance is wrapping + // ReSharper disable once MemberCanBeProtected.Global + public PostType Type { get; private set; } + + /// + /// If is set to true, this will hold the buffered data after + /// or is called + /// + public byte[] WrittenBytes { get; private set; } + + /// A static instance that represents a body with no data + // ReSharper disable once UnusedMember.Global + public static PostData Empty => new PostDataString(string.Empty); + + /// + /// Implementations of are expected to implement writing the data they hold to + /// + /// + public abstract void Write(Stream writableStream, ITransportConfiguration settings); + + /// + /// Implementations of are expected to implement writing the data they hold to + /// + /// + public abstract Task WriteAsync(Stream writableStream, ITransportConfiguration settings, + CancellationToken cancellationToken); + + /// + /// byte[] implicitly converts to so you do not have to use the static + /// factory method + /// + public static implicit operator PostData(byte[] byteArray) => Bytes(byteArray); + + /// Sets up the stream and buffer and determines if direct streaming should be disabled + // ReSharper disable once MemberCanBePrivate.Global + protected Stream InitWrite(Stream writableStream, ITransportConfiguration settings, out MemoryStream buffer, + out bool disableDirectStreaming) { - /// - /// The buffer size to use when calling - /// - // ReSharper disable once MemberCanBePrivate.Global - protected const int BufferSize = 81920; - - /// A static byte[] that hols a single new line feed - // ReSharper disable once MemberCanBePrivate.Global - protected static readonly byte[] NewLineByteArray = {(byte) '\n'}; - - /// - /// By setting this to true, and will buffer the data and - /// expose it on - /// - public bool? DisableDirectStreaming { get; set; } - - /// Reports the data this instance is wrapping - // ReSharper disable once MemberCanBeProtected.Global - public PostType Type { get; private set; } - - /// - /// If is set to true, this will hold the buffered data after - /// or is called - /// - public byte[] WrittenBytes { get; private set; } - - /// A static instance that represents a body with no data - // ReSharper disable once UnusedMember.Global - public static PostData Empty => new PostDataString(string.Empty); - - /// - /// Implementations of are expected to implement writing the data they hold to - /// - /// - public abstract void Write(Stream writableStream, ITransportConfiguration settings); - - /// - /// Implementations of are expected to implement writing the data they hold to - /// - /// - public abstract Task WriteAsync(Stream writableStream, ITransportConfiguration settings, - CancellationToken cancellationToken); - - /// - /// byte[] implicitly converts to so you do not have to use the static - /// factory method - /// - public static implicit operator PostData(byte[] byteArray) => Bytes(byteArray); - - /// Sets up the stream and buffer and determines if direct streaming should be disabled - // ReSharper disable once MemberCanBePrivate.Global - protected Stream InitWrite(Stream writableStream, ITransportConfiguration settings, out MemoryStream buffer, - out bool disableDirectStreaming) - { - buffer = null; - var stream = writableStream; - disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; - return stream; - } - - - /// - /// Based on or this will swap - /// with after allocating . - /// NOTE: is expected to be null when called and may be null when this method returns - /// - protected void BufferIfNeeded(ITransportConfiguration settings, ref MemoryStream buffer, - ref Stream stream) - { - var disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; - if (!disableDirectStreaming) return; - - buffer = settings.MemoryStreamFactory.Create(); - stream = buffer; - } - - /// - /// Implementation of may call this to make sure makes it to - /// if or request to buffer the data. - /// - protected void FinishStream(Stream writableStream, MemoryStream buffer, ITransportConfiguration settings) - { - var disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; - if (buffer == null || !disableDirectStreaming) return; - - buffer.Position = 0; - buffer.CopyTo(writableStream, BufferSize); - WrittenBytes ??= buffer.ToArray(); - } - - /// - /// Implementation of may call this to make sure makes it to - /// if or request to buffer the data. - /// - protected async + buffer = null; + var stream = writableStream; + disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; + return stream; + } + + + /// + /// Based on or this will swap + /// with after allocating . + /// NOTE: is expected to be null when called and may be null when this method returns + /// + protected void BufferIfNeeded(ITransportConfiguration settings, ref MemoryStream buffer, + ref Stream stream) + { + var disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; + if (!disableDirectStreaming) return; + + buffer = settings.MemoryStreamFactory.Create(); + stream = buffer; + } + + /// + /// Implementation of may call this to make sure makes it to + /// if or request to buffer the data. + /// + protected void FinishStream(Stream writableStream, MemoryStream buffer, ITransportConfiguration settings) + { + var disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; + if (buffer == null || !disableDirectStreaming) return; + + buffer.Position = 0; + buffer.CopyTo(writableStream, BufferSize); + WrittenBytes ??= buffer.ToArray(); + } + + /// + /// Implementation of may call this to make sure makes it to + /// if or request to buffer the data. + /// + protected async #if !NETSTANDARD2_0 && !NETFRAMEWORK - ValueTask + ValueTask #else - Task + Task #endif - FinishStreamAsync(Stream writableStream, MemoryStream buffer, ITransportConfiguration settings, - CancellationToken ctx) - { - var disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; - if (buffer == null || !disableDirectStreaming) return; - - buffer.Position = 0; - await buffer.CopyToAsync(writableStream, BufferSize, ctx).ConfigureAwait(false); - WrittenBytes ??= buffer.ToArray(); - } + FinishStreamAsync(Stream writableStream, MemoryStream buffer, ITransportConfiguration settings, + CancellationToken ctx) + { + var disableDirectStreaming = DisableDirectStreaming ?? settings.DisableDirectStreaming; + if (buffer == null || !disableDirectStreaming) return; + + buffer.Position = 0; + await buffer.CopyToAsync(writableStream, BufferSize, ctx).ConfigureAwait(false); + WrittenBytes ??= buffer.ToArray(); } } diff --git a/src/Elastic.Transport/Requests/Body/PostType.cs b/src/Elastic.Transport/Requests/Body/PostType.cs index 8158063..c69974f 100644 --- a/src/Elastic.Transport/Requests/Body/PostType.cs +++ b/src/Elastic.Transport/Requests/Body/PostType.cs @@ -6,54 +6,53 @@ using System; #endif -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Describes the type of data the user wants transmit over the transport +/// +public enum PostType { /// - /// Describes the type of data the user wants transmit over the transport + /// A raw array of 's to be send over the wire + /// Instantiate using /// - public enum PostType - { - /// - /// A raw array of 's to be send over the wire - /// Instantiate using - /// - ByteArray, + ByteArray, #if !NETSTANDARD2_0 && !NETFRAMEWORK - /// - /// An instance of where T is byte - /// Instantiate using - /// - ReadOnlyMemory, + /// + /// An instance of where T is byte + /// Instantiate using + /// + ReadOnlyMemory, #endif - /// - /// An instance of - /// Instantiate using - /// - LiteralString, - - /// - /// An instance of a user provided value to be serialized by - /// Instantiate using - /// - Serializable, - - /// - /// An enumerable of this will be serialized using ndjson multiline syntax - /// Instantiate using - /// - EnumerableOfString, - - /// - /// An enumerable of this will be serialized using ndjson multiline syntax - /// Instantiate using - /// - EnumerableOfObject, - - /// - /// The user provided a delegate to write the instance on manually and directly - /// Instantiate using - /// - StreamHandler, - - } + /// + /// An instance of + /// Instantiate using + /// + LiteralString, + + /// + /// An instance of a user provided value to be serialized by + /// Instantiate using + /// + Serializable, + + /// + /// An enumerable of this will be serialized using ndjson multiline syntax + /// Instantiate using + /// + EnumerableOfString, + + /// + /// An enumerable of this will be serialized using ndjson multiline syntax + /// Instantiate using + /// + EnumerableOfObject, + + /// + /// The user provided a delegate to write the instance on manually and directly + /// Instantiate using + /// + StreamHandler, + } diff --git a/src/Elastic.Transport/Requests/IRequestParameters.cs b/src/Elastic.Transport/Requests/IRequestParameters.cs deleted file mode 100644 index f09203e..0000000 --- a/src/Elastic.Transport/Requests/IRequestParameters.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Collections.Generic; - -// ReSharper disable once CheckNamespace -namespace Elastic.Transport -{ - /// - /// Per request specific parameters. Holds literal url request parameters as well as - /// per request configuration overrides on - /// - public interface IRequestParameters - { - /// Allows you to completely circumvent the serializer to build the final response. - CustomResponseBuilder CustomResponseBuilder { get; set; } - - /// - /// The querystring that should be appended to the path of the request - /// - Dictionary QueryString { get; set; } - - /// - /// Configuration for this specific request, i.e disable sniffing, custom timeouts etcetera. - /// - IRequestConfiguration RequestConfiguration { get; set; } - - /// Sets a query string param. If is null and the parameter exists it will be removed - /// The query string parameter to add - /// The value to set, if null removes from the query string if it exists - void SetQueryString(string name, object value); - - /// - /// Returns whether the query string was previously set already - /// - bool ContainsQueryString(string name); - - /// - /// Get's the value as its stored on the querystring using its original type - /// - TOut GetQueryStringValue(string name); - - /// - /// Gets the stringified representation of a query string value as it would be sent to Elasticsearch. - /// - string GetResolvedQueryStringValue(string name, ITransportConfiguration transportConfiguration); - - /// - /// Gets the HTTP Accept Header value from the shortened name. If the shortened name is not recognized, - /// null is returned. - /// - string AcceptHeaderFromFormat(string format); - } -} diff --git a/src/Elastic.Transport/Requests/IUrlParameter.cs b/src/Elastic.Transport/Requests/IUrlParameter.cs index d9c893e..ef4834b 100644 --- a/src/Elastic.Transport/Requests/IUrlParameter.cs +++ b/src/Elastic.Transport/Requests/IUrlParameter.cs @@ -2,12 +2,11 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport +namespace Elastic.Transport; + +/// Implementers define an object that can be serialized as a query string parameter +public interface IUrlParameter { - /// Implementers define an object that can be serialized as a query string parameter - public interface IUrlParameter - { - /// Get the a string representation using - string GetString(ITransportConfiguration settings); - } + /// Get the a string representation using + string GetString(ITransportConfiguration settings); } diff --git a/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs b/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs index 6fa9a72..03e61ec 100644 --- a/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs +++ b/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs @@ -4,61 +4,60 @@ using System; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public sealed class DefaultMetaHeaderProvider : MetaHeaderProvider { + private const string MetaHeaderName = "x-elastic-client-meta"; + + private readonly MetaDataHeader _asyncMetaDataHeader; + private readonly MetaDataHeader _syncMetaDataHeader; + /// /// /// - public sealed class DefaultMetaHeaderProvider : MetaHeaderProvider + public DefaultMetaHeaderProvider(Type clientType, string serviceIdentifier) { - private const string MetaHeaderName = "x-elastic-client-meta"; - - private readonly MetaDataHeader _asyncMetaDataHeader; - private readonly MetaDataHeader _syncMetaDataHeader; - - /// - /// - /// - public DefaultMetaHeaderProvider(Type clientType, string serviceIdentifier) - { - var clientVersionInfo = ReflectionVersionInfo.Create(clientType); - _asyncMetaDataHeader = new MetaDataHeader(clientVersionInfo, serviceIdentifier, true); - _syncMetaDataHeader = new MetaDataHeader(clientVersionInfo, serviceIdentifier, false); - } + var clientVersionInfo = ReflectionVersionInfo.Create(clientType); + _asyncMetaDataHeader = new MetaDataHeader(clientVersionInfo, serviceIdentifier, true); + _syncMetaDataHeader = new MetaDataHeader(clientVersionInfo, serviceIdentifier, false); + } - /// - /// - /// - public override string HeaderName => MetaHeaderName; + /// + /// + /// + public override string HeaderName => MetaHeaderName; - /// - /// - /// - /// - /// - public override string ProduceHeaderValue(RequestData requestData) + /// + /// + /// + /// + /// + public override string ProduceHeaderValue(RequestData requestData) + { + try { - try - { - if (requestData.ConnectionSettings.DisableMetaHeader) - return null; + if (requestData.ConnectionSettings.DisableMetaHeader) + return null; - var headerValue = requestData.IsAsync - ? _asyncMetaDataHeader.ToString() - : _syncMetaDataHeader.ToString(); + var headerValue = requestData.IsAsync + ? _asyncMetaDataHeader.ToString() + : _syncMetaDataHeader.ToString(); - // TODO - Cache values against key to avoid allocating a string each time - if (requestData.RequestMetaData.TryGetValue(RequestMetaData.HelperKey, out var helperSuffix)) - headerValue = $"{headerValue},h={helperSuffix}"; + // TODO - Cache values against key to avoid allocating a string each time + if (requestData.RequestMetaData.TryGetValue(RequestMetaData.HelperKey, out var helperSuffix)) + headerValue = $"{headerValue},h={helperSuffix}"; - return headerValue; - } - catch - { - // Don't fail the application just because we cannot create this optional header - } - - return string.Empty; + return headerValue; + } + catch + { + // Don't fail the application just because we cannot create this optional header } + + return string.Empty; } } diff --git a/src/Elastic.Transport/Requests/MetaData/MetaDataHeader.cs b/src/Elastic.Transport/Requests/MetaData/MetaDataHeader.cs index 8e284ea..83f7535 100644 --- a/src/Elastic.Transport/Requests/MetaData/MetaDataHeader.cs +++ b/src/Elastic.Transport/Requests/MetaData/MetaDataHeader.cs @@ -4,77 +4,76 @@ using System.Text; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public sealed class MetaDataHeader { + private const char _separator = ','; + + private readonly string _headerValue; + /// /// /// - public sealed class MetaDataHeader + /// + /// + /// + public MetaDataHeader(VersionInfo version, string serviceIdentifier, bool isAsync) { - private const char _separator = ','; + if (serviceIdentifier != "et") + TransportVersion = ReflectionVersionInfo.Create().ToString(); + + ClientVersion = version.ToString(); + RuntimeVersion = new RuntimeVersionInfo().ToString(); + ServiceIdentifier = serviceIdentifier; - private readonly string _headerValue; + // This code is expected to be called infrequently so we're not concerned with over optimising this - /// - /// - /// - /// - /// - /// - public MetaDataHeader(VersionInfo version, string serviceIdentifier, bool isAsync) - { - if (serviceIdentifier != "et") - TransportVersion = ReflectionVersionInfo.Create().ToString(); - - ClientVersion = version.ToString(); - RuntimeVersion = new RuntimeVersionInfo().ToString(); - ServiceIdentifier = serviceIdentifier; + var builder = new StringBuilder(64) + .Append(serviceIdentifier).Append("=").Append(ClientVersion).Append(_separator) + .Append("a=").Append(isAsync ? "1" : "0").Append(_separator) + .Append("net=").Append(RuntimeVersion).Append(_separator) + .Append(_httpClientIdentifier).Append("=").Append(RuntimeVersion); - // This code is expected to be called infrequently so we're not concerned with over optimising this + if (!string.IsNullOrEmpty(TransportVersion)) + builder.Append(_separator).Append("t=").Append(TransportVersion); - var builder = new StringBuilder(64) - .Append(serviceIdentifier).Append("=").Append(ClientVersion).Append(_separator) - .Append("a=").Append(isAsync ? "1" : "0").Append(_separator) - .Append("net=").Append(RuntimeVersion).Append(_separator) - .Append(_httpClientIdentifier).Append("=").Append(RuntimeVersion); - - if (!string.IsNullOrEmpty(TransportVersion)) - builder.Append(_separator).Append("t=").Append(TransportVersion); - - _headerValue = builder.ToString(); - } + _headerValue = builder.ToString(); + } - private static readonly string _httpClientIdentifier = + private static readonly string _httpClientIdentifier = #if DOTNETCORE - ConnectionInfo.UsingCurlHandler ? "cu" : "so"; + ConnectionInfo.UsingCurlHandler ? "cu" : "so"; #else - "wr"; + "wr"; #endif - /// - /// - /// - public string ServiceIdentifier { get; private set; } + /// + /// + /// + public string ServiceIdentifier { get; private set; } - /// - /// - /// - public string ClientVersion { get; private set; } + /// + /// + /// + public string ClientVersion { get; private set; } - /// - /// - /// - public string TransportVersion { get; private set; } + /// + /// + /// + public string TransportVersion { get; private set; } - /// - /// - /// - public string RuntimeVersion { get; private set; } + /// + /// + /// + public string RuntimeVersion { get; private set; } - /// - /// - /// - /// - public override string ToString() => _headerValue; - } + /// + /// + /// + /// + public override string ToString() => _headerValue; } diff --git a/src/Elastic.Transport/Requests/MetaData/MetaHeaderProvider.cs b/src/Elastic.Transport/Requests/MetaData/MetaHeaderProvider.cs new file mode 100644 index 0000000..9f58c0b --- /dev/null +++ b/src/Elastic.Transport/Requests/MetaData/MetaHeaderProvider.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Transport; + +/// +/// TODO +/// +public abstract class MetaHeaderProvider +{ + /// + /// + /// + public abstract string HeaderName { get; } + + /// + /// TODO + /// + /// + /// + public abstract string ProduceHeaderValue(RequestData requestData); +} diff --git a/src/Elastic.Transport/Requests/MetaData/MetaHeaderProviderBase.cs b/src/Elastic.Transport/Requests/MetaData/MetaHeaderProviderBase.cs deleted file mode 100644 index 96d8df5..0000000 --- a/src/Elastic.Transport/Requests/MetaData/MetaHeaderProviderBase.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.Transport -{ - /// - /// TODO - /// - public abstract class MetaHeaderProvider - { - /// - /// - /// - public abstract string HeaderName { get; } - - /// - /// TODO - /// - /// - /// - public abstract string ProduceHeaderValue(RequestData requestData); - } -} diff --git a/src/Elastic.Transport/Requests/MetaData/ReflectionVersionInfo.cs b/src/Elastic.Transport/Requests/MetaData/ReflectionVersionInfo.cs index cbfcee3..e55359b 100644 --- a/src/Elastic.Transport/Requests/MetaData/ReflectionVersionInfo.cs +++ b/src/Elastic.Transport/Requests/MetaData/ReflectionVersionInfo.cs @@ -7,51 +7,50 @@ using System.Reflection; using System.Text.RegularExpressions; -namespace Elastic.Transport +namespace Elastic.Transport; + +internal sealed class ReflectionVersionInfo : VersionInfo { - internal sealed class ReflectionVersionInfo : VersionInfo - { - private static readonly Regex VersionRegex = new(@"^\d+\.\d+\.\d\-?"); + private static readonly Regex VersionRegex = new(@"^\d+\.\d+\.\d\-?"); - public static readonly ReflectionVersionInfo Empty = new() { Version = new Version(0, 0, 0), IsPrerelease = false }; + public static readonly ReflectionVersionInfo Empty = new() { Version = new Version(0, 0, 0), IsPrerelease = false }; - private ReflectionVersionInfo() { } + private ReflectionVersionInfo() { } - public static ReflectionVersionInfo Create() - { - var fullVersion = DetermineVersionFromType(typeof(T)); - var clientVersion = new ReflectionVersionInfo(); - clientVersion.StoreVersion(fullVersion); - return clientVersion; - } + public static ReflectionVersionInfo Create() + { + var fullVersion = DetermineVersionFromType(typeof(T)); + var clientVersion = new ReflectionVersionInfo(); + clientVersion.StoreVersion(fullVersion); + return clientVersion; + } - public static ReflectionVersionInfo Create(Type type) - { - var fullVersion = DetermineVersionFromType(type); - var clientVersion = new ReflectionVersionInfo(); - clientVersion.StoreVersion(fullVersion); - return clientVersion; - } + public static ReflectionVersionInfo Create(Type type) + { + var fullVersion = DetermineVersionFromType(type); + var clientVersion = new ReflectionVersionInfo(); + clientVersion.StoreVersion(fullVersion); + return clientVersion; + } - private static string DetermineVersionFromType(Type type) + private static string DetermineVersionFromType(Type type) + { + try { - try - { - var productVersion = "8.0.0-alpha.8+02b315d290415a4eb153beb827a879d037e904f6 (Microsoft Windows 10.0.19044; .NET 6.0.4; Elastic.Clients.Elasticsearch)"; //FileVersionInfo.GetVersionInfo(type.GetTypeInfo().Assembly.Location)?.ProductVersion ?? EmptyVersion; - - if (productVersion == EmptyVersion) - productVersion = Assembly.GetAssembly(type).GetName().Version.ToString(); + var productVersion = "8.0.0-alpha.8+02b315d290415a4eb153beb827a879d037e904f6 (Microsoft Windows 10.0.19044; .NET 6.0.4; Elastic.Clients.Elasticsearch)"; //FileVersionInfo.GetVersionInfo(type.GetTypeInfo().Assembly.Location)?.ProductVersion ?? EmptyVersion; - var match = VersionRegex.Match(productVersion); + if (productVersion == EmptyVersion) + productVersion = Assembly.GetAssembly(type).GetName().Version.ToString(); - return match.Success ? match.Value : EmptyVersion; - } - catch - { - // ignore failures and fall through - } + var match = VersionRegex.Match(productVersion); - return EmptyVersion; + return match.Success ? match.Value : EmptyVersion; } + catch + { + // ignore failures and fall through + } + + return EmptyVersion; } } diff --git a/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs b/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs index 3432e59..b2a90fc 100644 --- a/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs +++ b/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs @@ -4,29 +4,28 @@ using System; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public static class RequestConfigurationExtensions { /// /// /// - public static class RequestConfigurationExtensions + /// + /// + /// + public static void SetRequestMetaData(this IRequestConfiguration requestConfiguration, RequestMetaData requestMetaData) { - /// - /// - /// - /// - /// - /// - public static void SetRequestMetaData(this IRequestConfiguration requestConfiguration, RequestMetaData requestMetaData) - { - if (requestConfiguration is null) - throw new ArgumentNullException(nameof(requestConfiguration)); + if (requestConfiguration is null) + throw new ArgumentNullException(nameof(requestConfiguration)); - if (requestMetaData is null) - throw new ArgumentNullException(nameof(requestMetaData)); + if (requestMetaData is null) + throw new ArgumentNullException(nameof(requestMetaData)); - requestConfiguration.RequestMetaData = requestMetaData; - } + requestConfiguration.RequestMetaData = requestMetaData; } - } + diff --git a/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs b/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs index a59eacc..551ea9f 100644 --- a/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs +++ b/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs @@ -5,38 +5,37 @@ using System.Collections.Generic; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Holds meta data about a client request. +/// +public sealed class RequestMetaData { /// - /// Holds meta data about a client request. + /// Reserved key for a meta data entry which identifies the helper which produced the request. /// - public sealed class RequestMetaData - { - /// - /// Reserved key for a meta data entry which identifies the helper which produced the request. - /// - internal const string HelperKey = "helper"; + internal const string HelperKey = "helper"; - private Dictionary _metaDataItems; + private Dictionary _metaDataItems; - internal bool TryAddMetaData(string key, string value) - { - _metaDataItems ??= new Dictionary(); + internal bool TryAddMetaData(string key, string value) + { + _metaDataItems ??= new Dictionary(); #if NETSTANDARD2_1 - return _metaDataItems.TryAdd(key, value); + return _metaDataItems.TryAdd(key, value); #else - if (_metaDataItems.ContainsKey(key)) - return false; + if (_metaDataItems.ContainsKey(key)) + return false; - _metaDataItems.Add(key, value); - return true; + _metaDataItems.Add(key, value); + return true; #endif - } - - /// - /// - /// - public IReadOnlyDictionary Items => _metaDataItems ?? EmptyReadOnly.Dictionary; } + + /// + /// + /// + public IReadOnlyDictionary Items => _metaDataItems ?? EmptyReadOnly.Dictionary; } diff --git a/src/Elastic.Transport/Requests/MetaData/RequestMetaDataExtensions.cs b/src/Elastic.Transport/Requests/MetaData/RequestMetaDataExtensions.cs index 80edf4e..4217bdf 100644 --- a/src/Elastic.Transport/Requests/MetaData/RequestMetaDataExtensions.cs +++ b/src/Elastic.Transport/Requests/MetaData/RequestMetaDataExtensions.cs @@ -4,24 +4,23 @@ using System; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public static class RequestMetaDataExtensions { /// /// /// - public static class RequestMetaDataExtensions + /// + /// + /// + public static void AddHelper(this RequestMetaData metaData, string helperValue) { - /// - /// - /// - /// - /// - /// - public static void AddHelper(this RequestMetaData metaData, string helperValue) - { - if (!metaData.TryAddMetaData(RequestMetaData.HelperKey, helperValue)) - throw new InvalidOperationException("A helper value has already been added."); - } + if (!metaData.TryAddMetaData(RequestMetaData.HelperKey, helperValue)) + throw new InvalidOperationException("A helper value has already been added."); } - } + diff --git a/src/Elastic.Transport/Requests/MetaData/RuntimeVersionInfo.cs b/src/Elastic.Transport/Requests/MetaData/RuntimeVersionInfo.cs index f6fca3a..290600f 100644 --- a/src/Elastic.Transport/Requests/MetaData/RuntimeVersionInfo.cs +++ b/src/Elastic.Transport/Requests/MetaData/RuntimeVersionInfo.cs @@ -35,210 +35,209 @@ using System.Linq; #endif -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Represents the current .NET Runtime version. +/// +internal sealed class RuntimeVersionInfo : VersionInfo { - /// - /// Represents the current .NET Runtime version. - /// - internal sealed class RuntimeVersionInfo : VersionInfo - { - public static readonly RuntimeVersionInfo Default = new RuntimeVersionInfo { Version = new Version(0, 0, 0), IsPrerelease = false }; + public static readonly RuntimeVersionInfo Default = new RuntimeVersionInfo { Version = new Version(0, 0, 0), IsPrerelease = false }; - public RuntimeVersionInfo() => StoreVersion(GetRuntimeVersion()); + public RuntimeVersionInfo() => StoreVersion(GetRuntimeVersion()); - private static string GetRuntimeVersion() => + private static string GetRuntimeVersion() => #if !DOTNETCORE - GetFullFrameworkRuntime(); + GetFullFrameworkRuntime(); #else - GetNetCoreVersion(); + GetNetCoreVersion(); #endif #if DOTNETCORE - private static string GetNetCoreVersion() + private static string GetNetCoreVersion() + { + // for .NET 5+ we can use Environment.Version + if (Environment.Version.Major >= 5) { - // for .NET 5+ we can use Environment.Version - if (Environment.Version.Major >= 5) - { - const string dotNet = ".NET "; - var index = RuntimeInformation.FrameworkDescription.IndexOf(dotNet, StringComparison.OrdinalIgnoreCase); - if (index >= 0) - { - return RuntimeInformation.FrameworkDescription.Substring(dotNet.Length); - } - } - - // next, try using file version info - var systemPrivateCoreLib = FileVersionInfo.GetVersionInfo(typeof(object).Assembly.Location); - if (TryGetVersionFromProductInfo(systemPrivateCoreLib.ProductVersion, systemPrivateCoreLib.ProductName, out var runtimeVersion)) - { - return runtimeVersion; - } - - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - if (TryGetVersionFromAssemblyPath(assembly, out runtimeVersion)) + const string dotNet = ".NET "; + var index = RuntimeInformation.FrameworkDescription.IndexOf(dotNet, StringComparison.OrdinalIgnoreCase); + if (index >= 0) { - return runtimeVersion; + return RuntimeInformation.FrameworkDescription.Substring(dotNet.Length); } + } - //At this point, we can't identify whether this is a prerelease, but a version is better than nothing! - - var frameworkName = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; - if (TryGetVersionFromFrameworkName(frameworkName, out runtimeVersion)) - { - return runtimeVersion; - } + // next, try using file version info + var systemPrivateCoreLib = FileVersionInfo.GetVersionInfo(typeof(object).Assembly.Location); + if (TryGetVersionFromProductInfo(systemPrivateCoreLib.ProductVersion, systemPrivateCoreLib.ProductName, out var runtimeVersion)) + { + return runtimeVersion; + } - if (IsRunningInContainer) - { - var dotNetVersion = Environment.GetEnvironmentVariable("DOTNET_VERSION"); - var aspNetCoreVersion = Environment.GetEnvironmentVariable("ASPNETCORE_VERSION"); + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + if (TryGetVersionFromAssemblyPath(assembly, out runtimeVersion)) + { + return runtimeVersion; + } - return dotNetVersion ?? aspNetCoreVersion; - } + //At this point, we can't identify whether this is a prerelease, but a version is better than nothing! - return null; + var frameworkName = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; + if (TryGetVersionFromFrameworkName(frameworkName, out runtimeVersion)) + { + return runtimeVersion; } - private static bool TryGetVersionFromAssemblyPath(Assembly assembly, out string runtimeVersion) + if (IsRunningInContainer) { - var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - var netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - runtimeVersion = assemblyPath[netCoreAppIndex + 1]; - return true; - } + var dotNetVersion = Environment.GetEnvironmentVariable("DOTNET_VERSION"); + var aspNetCoreVersion = Environment.GetEnvironmentVariable("ASPNETCORE_VERSION"); - runtimeVersion = null; - return false; + return dotNetVersion ?? aspNetCoreVersion; } - // NOTE: 5.0.1 FrameworkDescription returns .NET 5.0.1-servicing.20575.16, so we special case servicing as NOT prerelease - protected override bool ContainsPrerelease(string version) => base.ContainsPrerelease(version) && !version.Contains("-servicing"); + return null; + } - // sample input: - // 2.0: 4.6.26614.01 @BuiltBy: dlab14-DDVSOWINAGE018 @Commit: a536e7eec55c538c94639cefe295aa672996bf9b, Microsoft .NET Framework - // 2.1: 4.6.27817.01 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.1 @SrcCode: https://github.com/dotnet/coreclr/tree/6f78fbb3f964b4f407a2efb713a186384a167e5c, Microsoft .NET Framework - // 2.2: 4.6.27817.03 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.2 @SrcCode: https://github.com/dotnet/coreclr/tree/ce1d090d33b400a25620c0145046471495067cc7, Microsoft .NET Framework - // 3.0: 3.0.0-preview8.19379.2+ac25be694a5385a6a1496db40de932df0689b742, Microsoft .NET Core - // 5.0: 5.0.0-alpha1.19413.7+0ecefa44c9d66adb8a997d5778dc6c246ad393a7, Microsoft .NET Core - private static bool TryGetVersionFromProductInfo(string productVersion, string productName, out string version) + private static bool TryGetVersionFromAssemblyPath(Assembly assembly, out string runtimeVersion) + { + var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + var netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) { - if (string.IsNullOrEmpty(productVersion) || string.IsNullOrEmpty(productName)) - { - version = null; - return false; - } + runtimeVersion = assemblyPath[netCoreAppIndex + 1]; + return true; + } - // yes, .NET Core 2.X has a product name == .NET Framework... - if (productName.IndexOf(".NET Framework", StringComparison.OrdinalIgnoreCase) >= 0) - { - const string releaseVersionPrefix = "release/"; - var releaseVersionIndex = productVersion.IndexOf(releaseVersionPrefix); - if (releaseVersionIndex > 0) - { - version = productVersion.Substring(releaseVersionIndex + releaseVersionPrefix.Length); - return true; - } - } + runtimeVersion = null; + return false; + } - // matches .NET Core and also .NET 5+ - if (productName.IndexOf(".NET", StringComparison.OrdinalIgnoreCase) >= 0) - { - version = productVersion; - return true; - } + // NOTE: 5.0.1 FrameworkDescription returns .NET 5.0.1-servicing.20575.16, so we special case servicing as NOT prerelease + protected override bool ContainsPrerelease(string version) => base.ContainsPrerelease(version) && !version.Contains("-servicing"); + // sample input: + // 2.0: 4.6.26614.01 @BuiltBy: dlab14-DDVSOWINAGE018 @Commit: a536e7eec55c538c94639cefe295aa672996bf9b, Microsoft .NET Framework + // 2.1: 4.6.27817.01 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.1 @SrcCode: https://github.com/dotnet/coreclr/tree/6f78fbb3f964b4f407a2efb713a186384a167e5c, Microsoft .NET Framework + // 2.2: 4.6.27817.03 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.2 @SrcCode: https://github.com/dotnet/coreclr/tree/ce1d090d33b400a25620c0145046471495067cc7, Microsoft .NET Framework + // 3.0: 3.0.0-preview8.19379.2+ac25be694a5385a6a1496db40de932df0689b742, Microsoft .NET Core + // 5.0: 5.0.0-alpha1.19413.7+0ecefa44c9d66adb8a997d5778dc6c246ad393a7, Microsoft .NET Core + private static bool TryGetVersionFromProductInfo(string productVersion, string productName, out string version) + { + if (string.IsNullOrEmpty(productVersion) || string.IsNullOrEmpty(productName)) + { version = null; return false; } - // sample input: - // .NETCoreApp,Version=v2.0 - // .NETCoreApp,Version=v2.1 - private static bool TryGetVersionFromFrameworkName(string frameworkName, out string runtimeVersion) + // yes, .NET Core 2.X has a product name == .NET Framework... + if (productName.IndexOf(".NET Framework", StringComparison.OrdinalIgnoreCase) >= 0) { - const string versionPrefix = ".NETCoreApp,Version=v"; - if (!string.IsNullOrEmpty(frameworkName) && frameworkName.StartsWith(versionPrefix)) + const string releaseVersionPrefix = "release/"; + var releaseVersionIndex = productVersion.IndexOf(releaseVersionPrefix); + if (releaseVersionIndex > 0) { - runtimeVersion = frameworkName.Substring(versionPrefix.Length); + version = productVersion.Substring(releaseVersionIndex + releaseVersionPrefix.Length); return true; } + } - runtimeVersion = null; - return false; + // matches .NET Core and also .NET 5+ + if (productName.IndexOf(".NET", StringComparison.OrdinalIgnoreCase) >= 0) + { + version = productVersion; + return true; + } + + version = null; + return false; + } + + // sample input: + // .NETCoreApp,Version=v2.0 + // .NETCoreApp,Version=v2.1 + private static bool TryGetVersionFromFrameworkName(string frameworkName, out string runtimeVersion) + { + const string versionPrefix = ".NETCoreApp,Version=v"; + if (!string.IsNullOrEmpty(frameworkName) && frameworkName.StartsWith(versionPrefix)) + { + runtimeVersion = frameworkName.Substring(versionPrefix.Length); + return true; } - private static bool IsRunningInContainer => string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true"); + runtimeVersion = null; + return false; + } + + private static bool IsRunningInContainer => string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true"); #endif #if !DOTNETCORE - private static string GetFullFrameworkRuntime() - { - const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; + private static string GetFullFrameworkRuntime() + { + const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; - using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey)) + using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey)) + { + if (ndpKey != null && ndpKey.GetValue("Release") != null) { - if (ndpKey != null && ndpKey.GetValue("Release") != null) - { - var version = CheckFor45PlusVersion((int)ndpKey.GetValue("Release")); + var version = CheckFor45PlusVersion((int)ndpKey.GetValue("Release")); - if (!string.IsNullOrEmpty(version) ) - return version; - } + if (!string.IsNullOrEmpty(version) ) + return version; } + } - var fullName = RuntimeInformation.FrameworkDescription; - var servicingVersion = new string(fullName.SkipWhile(c => !char.IsDigit(c)).ToArray()); - var servicingVersionRelease = MapToReleaseVersion(servicingVersion); + var fullName = RuntimeInformation.FrameworkDescription; + var servicingVersion = new string(fullName.SkipWhile(c => !char.IsDigit(c)).ToArray()); + var servicingVersionRelease = MapToReleaseVersion(servicingVersion); - return servicingVersionRelease; + return servicingVersionRelease; - static string MapToReleaseVersion(string servicingVersion) - { - // the following code assumes that .NET 4.6.1 is the oldest supported version - if (string.Compare(servicingVersion, "4.6.2") < 0) - return "4.6.1"; - if (string.Compare(servicingVersion, "4.7") < 0) - return "4.6.2"; - if (string.Compare(servicingVersion, "4.7.1") < 0) - return "4.7"; - if (string.Compare(servicingVersion, "4.7.2") < 0) - return "4.7.1"; - if (string.Compare(servicingVersion, "4.8") < 0) - return "4.7.2"; - - return "4.8.0"; // most probably the last major release of Full .NET Framework - } + static string MapToReleaseVersion(string servicingVersion) + { + // the following code assumes that .NET 4.6.1 is the oldest supported version + if (string.Compare(servicingVersion, "4.6.2") < 0) + return "4.6.1"; + if (string.Compare(servicingVersion, "4.7") < 0) + return "4.6.2"; + if (string.Compare(servicingVersion, "4.7.1") < 0) + return "4.7"; + if (string.Compare(servicingVersion, "4.7.2") < 0) + return "4.7.1"; + if (string.Compare(servicingVersion, "4.8") < 0) + return "4.7.2"; + + return "4.8.0"; // most probably the last major release of Full .NET Framework + } - // Checking the version using >= enables forward compatibility. - static string CheckFor45PlusVersion(int releaseKey) - { - if (releaseKey >= 528040) - return "4.8.0"; - if (releaseKey >= 461808) - return "4.7.2"; - if (releaseKey >= 461308) - return "4.7.1"; - if (releaseKey >= 460798) - return "4.7"; - if (releaseKey >= 394802) - return "4.6.2"; - if (releaseKey >= 394254) - return "4.6.1"; - if (releaseKey >= 393295) - return "4.6"; - if (releaseKey >= 379893) - return "4.5.2"; - if (releaseKey >= 378675) - return "4.5.1"; - if (releaseKey >= 378389) - return "4.5.0"; - // This code should never execute. A non-null release key should mean - // that 4.5 or later is installed. - return null; - } + // Checking the version using >= enables forward compatibility. + static string CheckFor45PlusVersion(int releaseKey) + { + if (releaseKey >= 528040) + return "4.8.0"; + if (releaseKey >= 461808) + return "4.7.2"; + if (releaseKey >= 461308) + return "4.7.1"; + if (releaseKey >= 460798) + return "4.7"; + if (releaseKey >= 394802) + return "4.6.2"; + if (releaseKey >= 394254) + return "4.6.1"; + if (releaseKey >= 393295) + return "4.6"; + if (releaseKey >= 379893) + return "4.5.2"; + if (releaseKey >= 378675) + return "4.5.1"; + if (releaseKey >= 378389) + return "4.5.0"; + // This code should never execute. A non-null release key should mean + // that 4.5 or later is installed. + return null; } -#endif } +#endif } diff --git a/src/Elastic.Transport/Requests/MetaData/VersionInfo.cs b/src/Elastic.Transport/Requests/MetaData/VersionInfo.cs index 8e52ca5..6f79d56 100644 --- a/src/Elastic.Transport/Requests/MetaData/VersionInfo.cs +++ b/src/Elastic.Transport/Requests/MetaData/VersionInfo.cs @@ -5,75 +5,74 @@ using System; using System.Linq; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public abstract class VersionInfo { /// /// /// - public abstract class VersionInfo - { - /// - /// - /// - protected const string EmptyVersion = "0.0.0"; + protected const string EmptyVersion = "0.0.0"; - /// - /// - /// - public Version Version { get; protected set; } + /// + /// + /// + public Version Version { get; protected set; } - /// - /// - /// - public bool IsPrerelease { get; protected set; } + /// + /// + /// + public bool IsPrerelease { get; protected set; } - /// - /// - /// - /// - /// - protected void StoreVersion(string fullVersion) - { - if (string.IsNullOrEmpty(fullVersion)) - fullVersion = EmptyVersion; + /// + /// + /// + /// + /// + protected void StoreVersion(string fullVersion) + { + if (string.IsNullOrEmpty(fullVersion)) + fullVersion = EmptyVersion; - var clientVersion = GetParsableVersionPart(fullVersion); + var clientVersion = GetParsableVersionPart(fullVersion); - if (!Version.TryParse(clientVersion, out var parsedVersion)) - throw new ArgumentException("Invalid version string", nameof(fullVersion)); + if (!Version.TryParse(clientVersion, out var parsedVersion)) + throw new ArgumentException("Invalid version string", nameof(fullVersion)); - var finalVersion = parsedVersion; + var finalVersion = parsedVersion; - if (parsedVersion.Minor == -1 || parsedVersion.Build == -1) - finalVersion = new Version(parsedVersion.Major, parsedVersion.Minor > -1 - ? parsedVersion.Minor - : 0, parsedVersion.Build > -1 - ? parsedVersion.Build - : 0); + if (parsedVersion.Minor == -1 || parsedVersion.Build == -1) + finalVersion = new Version(parsedVersion.Major, parsedVersion.Minor > -1 + ? parsedVersion.Minor + : 0, parsedVersion.Build > -1 + ? parsedVersion.Build + : 0); - Version = finalVersion; - IsPrerelease = ContainsPrerelease(fullVersion); - } + Version = finalVersion; + IsPrerelease = ContainsPrerelease(fullVersion); + } - /// - /// - /// - /// - /// - protected virtual bool ContainsPrerelease(string version) => version.Contains("-"); + /// + /// + /// + /// + /// + protected virtual bool ContainsPrerelease(string version) => version.Contains("-"); - /// - /// - /// - /// - /// - private static string GetParsableVersionPart(string fullVersionName) => - new(fullVersionName.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray()); + /// + /// + /// + /// + /// + private static string GetParsableVersionPart(string fullVersionName) => + new(fullVersionName.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray()); - /// - /// - /// - /// - public override string ToString() => IsPrerelease ? Version.ToString() + "p" : Version.ToString(); - } + /// + /// + /// + /// + public override string ToString() => IsPrerelease ? Version.ToString() + "p" : Version.ToString(); } diff --git a/src/Elastic.Transport/Requests/RequestParameters.cs b/src/Elastic.Transport/Requests/RequestParameters.cs index f40e452..be4d0ad 100644 --- a/src/Elastic.Transport/Requests/RequestParameters.cs +++ b/src/Elastic.Transport/Requests/RequestParameters.cs @@ -6,115 +6,129 @@ using static Elastic.Transport.UrlFormatter; // ReSharper disable once CheckNamespace -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public interface IStringable { /// /// /// - public interface IStringable - { - /// - /// - /// - /// - string GetString(); - } + /// + string GetString(); +} - /// - public class RequestParameters : RequestParameters - { - } +/// +/// +/// +public sealed class DefaultRequestParameters : RequestParameters +{ +} +/// +/// Used by the raw client to compose querystring parameters in a matter that still exposes some xmldocs +/// You can always pass a simple NameValueCollection if you want. +/// +public abstract class RequestParameters +{ /// - /// Used by the raw client to compose querystring parameters in a matter that still exposes some xmldocs - /// You can always pass a simple NameValueCollection if you want. + /// /// - /// - public abstract class RequestParameters : IRequestParameters where T : RequestParameters - { - /// - public CustomResponseBuilder CustomResponseBuilder { get; set; } + public CustomResponseBuilder CustomResponseBuilder { get; internal set; } - /// - public Dictionary QueryString { get; set; } = new Dictionary(); + /// + /// + /// + public Dictionary QueryString { get; internal set; } = new Dictionary(); - /// - public IRequestConfiguration RequestConfiguration { get; set; } + /// + /// + /// + public IRequestConfiguration RequestConfiguration { get; set; } - private IRequestParameters Self => this; + /// + /// + /// + public bool ContainsQueryString(string name) => QueryString != null && QueryString.ContainsKey(name); - /// - public bool ContainsQueryString(string name) => Self.QueryString != null && Self.QueryString.ContainsKey(name); + /// + /// + /// + public TOut GetQueryStringValue(string name) + { + if (!ContainsQueryString(name)) + return default; - /// - public TOut GetQueryStringValue(string name) - { - if (!ContainsQueryString(name)) - return default; + var value = QueryString[name]; + if (value == null) + return default; - var value = Self.QueryString[name]; - if (value == null) - return default; + return (TOut)value; + } - return (TOut)value; - } + /// + /// + /// + public string GetResolvedQueryStringValue(string name, ITransportConfiguration transportConfiguration) => + CreateString(GetQueryStringValue(name), transportConfiguration); - /// - public string GetResolvedQueryStringValue(string name, ITransportConfiguration transportConfiguration) => - CreateString(GetQueryStringValue(name), transportConfiguration); + /// + /// + /// + public void SetQueryString(string name, object value) + { + if (value == null) RemoveQueryString(name); + else QueryString[name] = value; + } - /// - public void SetQueryString(string name, object value) - { - if (value == null) RemoveQueryString(name); - else Self.QueryString[name] = value; - } + /// Shortcut to for generated code + protected TOut Q(string name) => GetQueryStringValue(name); - /// Shortcut to for generated code - protected TOut Q(string name) => GetQueryStringValue(name); + /// Shortcut to for generated code + protected void Q(string name, object value) => SetQueryString(name, value); - /// Shortcut to for generated code - protected void Q(string name, object value) => SetQueryString(name, value); + /// Shortcut to for generated code + protected void Q(string name, IStringable value) => SetQueryString(name, value.GetString()); - /// Shortcut to for generated code - protected void Q(string name, IStringable value) => SetQueryString(name, value.GetString()); + private void RemoveQueryString(string name) + { + if (!QueryString.ContainsKey(name)) return; - private void RemoveQueryString(string name) - { - if (!Self.QueryString.ContainsKey(name)) return; + QueryString.Remove(name); + } - Self.QueryString.Remove(name); - } + /// + /// Makes sure is set before explicitly setting + /// + protected void SetAcceptHeader(string format) + { + if (RequestConfiguration == null) + RequestConfiguration = new RequestConfiguration(); - /// - /// Makes sure is set before explicitly setting - /// - protected void SetAcceptHeader(string format) - { - if (RequestConfiguration == null) - RequestConfiguration = new RequestConfiguration(); + RequestConfiguration.Accept = AcceptHeaderFromFormat(format); + } - RequestConfiguration.Accept = AcceptHeaderFromFormat(format); - } + /// + /// + /// + public string AcceptHeaderFromFormat(string format) + { + if (format == null) + return null; + + var lowerFormat = format.ToLowerInvariant(); - /// - public string AcceptHeaderFromFormat(string format) + switch(lowerFormat) { - if (format == null) + case "smile": + case "yaml": + case "cbor": + case "json": + return $"application/{lowerFormat}"; + default: return null; - - var lowerFormat = format.ToLowerInvariant(); - - switch(lowerFormat) - { - case "smile": - case "yaml": - case "cbor": - case "json": - return $"application/{lowerFormat}"; - default: - return null; - } } } } diff --git a/src/Elastic.Transport/Requests/UrlFormatter.cs b/src/Elastic.Transport/Requests/UrlFormatter.cs index e85b8b5..79c2809 100644 --- a/src/Elastic.Transport/Requests/UrlFormatter.cs +++ b/src/Elastic.Transport/Requests/UrlFormatter.cs @@ -7,59 +7,58 @@ using System.Linq; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A formatter that can utilize to resolve 's passed +/// as format arguments. It also handles known string representations for e.g bool/Enums/IEnumerable. +/// +public sealed class UrlFormatter : IFormatProvider, ICustomFormatter { - /// - /// A formatter that can utilize to resolve 's passed - /// as format arguments. It also handles known string representations for e.g bool/Enums/IEnumerable. - /// - public sealed class UrlFormatter : IFormatProvider, ICustomFormatter - { - private readonly ITransportConfiguration _settings; + private readonly ITransportConfiguration _settings; - /// - public UrlFormatter(ITransportConfiguration settings) => _settings = settings; + /// + public UrlFormatter(ITransportConfiguration settings) => _settings = settings; - /// > - public string Format(string format, object arg, IFormatProvider formatProvider) - { - if (arg == null) throw new ArgumentNullException(); + /// > + public string Format(string format, object arg, IFormatProvider formatProvider) + { + if (arg == null) throw new ArgumentNullException(); - if (format == "r") return arg.ToString(); + if (format == "r") return arg.ToString(); - var value = CreateString(arg, _settings); - if (value.IsNullOrEmpty() && !format.IsNullOrEmpty()) - throw new ArgumentException($"The parameter: {format} to the url is null or empty"); + var value = CreateString(arg, _settings); + if (value.IsNullOrEmpty() && !format.IsNullOrEmpty()) + throw new ArgumentException($"The parameter: {format} to the url is null or empty"); - return value.IsNullOrEmpty() ? string.Empty : Uri.EscapeDataString(value); - } + return value.IsNullOrEmpty() ? string.Empty : Uri.EscapeDataString(value); + } - /// - public object GetFormat(Type formatType) => formatType == typeof(ICustomFormatter) ? this : null; + /// + public object GetFormat(Type formatType) => formatType == typeof(ICustomFormatter) ? this : null; - /// - public string CreateString(object value) => CreateString(value, _settings); + /// + public string CreateString(object value) => CreateString(value, _settings); - /// Creates a query string representation for - public static string CreateString(object value, ITransportConfiguration settings) + /// Creates a query string representation for + public static string CreateString(object value, ITransportConfiguration settings) + { + switch (value) { - switch (value) - { - case null: return null; - case string s: return s; - case string[] ss: return string.Join(",", ss); - case Enum e: return e.GetStringValue(); - case bool b: return b ? "true" : "false"; - case DateTimeOffset offset: return offset.ToString("o"); - case IEnumerable pns: - return string.Join(",", pns.Select(o => ResolveUrlParameterOrDefault(o, settings))); - case TimeSpan timeSpan: return timeSpan.ToTimeUnit(); - default: - return ResolveUrlParameterOrDefault(value, settings); - } + case null: return null; + case string s: return s; + case string[] ss: return string.Join(",", ss); + case Enum e: return e.GetStringValue(); + case bool b: return b ? "true" : "false"; + case DateTimeOffset offset: return offset.ToString("o"); + case IEnumerable pns: + return string.Join(",", pns.Select(o => ResolveUrlParameterOrDefault(o, settings))); + case TimeSpan timeSpan: return timeSpan.ToTimeUnit(); + default: + return ResolveUrlParameterOrDefault(value, settings); } - - private static string ResolveUrlParameterOrDefault(object value, ITransportConfiguration settings) => - value is IUrlParameter urlParam ? urlParam.GetString(settings) : value.ToString(); } + + private static string ResolveUrlParameterOrDefault(object value, ITransportConfiguration settings) => + value is IUrlParameter urlParam ? urlParam.GetString(settings) : value.ToString(); } diff --git a/src/Elastic.Transport/Responses/CustomResponseBuilder.cs b/src/Elastic.Transport/Responses/CustomResponseBuilder.cs index 0161388..49383df 100644 --- a/src/Elastic.Transport/Responses/CustomResponseBuilder.cs +++ b/src/Elastic.Transport/Responses/CustomResponseBuilder.cs @@ -6,19 +6,18 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Allows callers of to override completely +/// how `TResponse` should be deserialized to a `TResponse` that implements instance. +/// Expert setting only +/// +public abstract class CustomResponseBuilder { - /// - /// Allows callers of to override completely - /// how `TResponse` should be deserialized to a `TResponse` that implements instance. - /// Expert setting only - /// - public abstract class CustomResponseBuilder - { - /// Custom routine that deserializes from to an instance of . - public abstract object DeserializeResponse(Serializer builtInSerializer, IApiCallDetails response, Stream stream); + /// Custom routine that deserializes from to an instance of . + public abstract object DeserializeResponse(Serializer serializer, ApiCallDetails response, Stream stream); - /// - public abstract Task DeserializeResponseAsync(Serializer builtInSerializer, IApiCallDetails response, Stream stream, CancellationToken ctx = default); - } + /// + public abstract Task DeserializeResponseAsync(Serializer serializer, ApiCallDetails response, Stream stream, CancellationToken ctx = default); } diff --git a/src/Elastic.Transport/Responses/Dynamic/DynamicDictionary.cs b/src/Elastic.Transport/Responses/Dynamic/DynamicDictionary.cs index a5999aa..7bad3ff 100644 --- a/src/Elastic.Transport/Responses/Dynamic/DynamicDictionary.cs +++ b/src/Elastic.Transport/Responses/Dynamic/DynamicDictionary.cs @@ -19,352 +19,351 @@ // ReSharper disable RemoveRedundantBraces // ReSharper disable ArrangeAccessorOwnerBody -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A dictionary that supports dynamic access. +/// +public sealed class DynamicDictionary + : DynamicObject, + IEquatable, + IDictionary { + private readonly IDictionary _backingDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// - /// A dictionary that supports dynamic access. + /// Gets the number of elements contained in the . /// - public sealed class DynamicDictionary - : DynamicObject, - IEquatable, - IDictionary + /// The number of elements contained in the . + public int Count => _backingDictionary.Count; + + /// + /// Creates a new instance of Dictionary{String,Object} using the keys and underlying object values of this DynamicDictionary instance's key values. + /// + /// + public Dictionary ToDictionary() => + _backingDictionary.ToDictionary(kv => kv.Key, kv => kv.Value.Value is JsonElement e ? DynamicValue.ConsumeJsonElement(typeof(object), e) : kv.Value.Value); + + /// + /// Returns an empty dynamic dictionary. + /// + /// A instance. + public static DynamicDictionary Empty => new DynamicDictionary(); + + /// + /// Gets a value indicating whether the is read-only. + /// + /// Always returns . + public bool IsReadOnly => false; + + private static readonly Regex SplitRegex = new(@"(? + /// Traverses data using path notation. + /// e.g some.deep.nested.json.path + /// + /// A special lookup is available for ANY key _arbitrary_key_ e.g some.deep._arbitrary_key_.json.path which will traverse into the first key + /// If _arbitrary_key_ is the last value it will return the key name + /// + /// + /// path into the stored object, keys are separated with a dot and the last key is returned as T + /// + /// T or default + public T Get(string path) { - private readonly IDictionary _backingDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets the number of elements contained in the . - /// - /// The number of elements contained in the . - public int Count => _backingDictionary.Count; - - /// - /// Creates a new instance of Dictionary{String,Object} using the keys and underlying object values of this DynamicDictionary instance's key values. - /// - /// - public Dictionary ToDictionary() => - _backingDictionary.ToDictionary(kv => kv.Key, kv => kv.Value.Value is JsonElement e ? DynamicValue.ConsumeJsonElement(typeof(object), e) : kv.Value.Value); - - /// - /// Returns an empty dynamic dictionary. - /// - /// A instance. - public static DynamicDictionary Empty => new DynamicDictionary(); - - /// - /// Gets a value indicating whether the is read-only. - /// - /// Always returns . - public bool IsReadOnly => false; - - private static readonly Regex SplitRegex = new(@"(? - /// Traverses data using path notation. - /// e.g some.deep.nested.json.path - /// - /// A special lookup is available for ANY key _arbitrary_key_ e.g some.deep._arbitrary_key_.json.path which will traverse into the first key - /// If _arbitrary_key_ is the last value it will return the key name - /// - /// - /// path into the stored object, keys are separated with a dot and the last key is returned as T - /// - /// T or default - public T Get(string path) - { - if (path == null) return default; + if (path == null) return default; - var split = SplitRegex.Split(path); - var queue = new Queue(split); - if (queue.Count == 0) return default; + var split = SplitRegex.Split(path); + var queue = new Queue(split); + if (queue.Count == 0) return default; - var d = new DynamicValue(_backingDictionary); - while (queue.Count > 0) + var d = new DynamicValue(_backingDictionary); + while (queue.Count > 0) + { + var key = queue.Dequeue().Replace(@"\.", "."); + if (key == "_arbitrary_key_") { - var key = queue.Dequeue().Replace(@"\.", "."); - if (key == "_arbitrary_key_") + if (queue.Count > 0) d = d[0]; + else { - if (queue.Count > 0) d = d[0]; - else - { - var v = d?.ToDictionary()?.Keys.FirstOrDefault(); - d = v != null ? new DynamicValue(v) : DynamicValue.NullValue; - } + var v = d?.ToDictionary()?.Keys.FirstOrDefault(); + d = v != null ? new DynamicValue(v) : DynamicValue.NullValue; } - else if (int.TryParse(key, out var i)) d = d[i]; - else d = d[key]; } - - return d.TryParse(); + else if (int.TryParse(key, out var i)) d = d[i]; + else d = d[key]; } - /// - /// Gets or sets the with the specified name. - /// - /// A instance containing a value. - public DynamicValue this[string name] - { - get - { - name = GetNeutralKey(name); + return d.TryParse(); + } - if (!_backingDictionary.TryGetValue(name, out var member)) - { - member = new DynamicValue(null); - } + /// + /// Gets or sets the with the specified name. + /// + /// A instance containing a value. + public DynamicValue this[string name] + { + get + { + name = GetNeutralKey(name); - return member; - } - set + if (!_backingDictionary.TryGetValue(name, out var member)) { - name = GetNeutralKey(name); - - _backingDictionary[name] = value ?? DynamicValue.NullValue; + member = new DynamicValue(null); } - } - /// - /// Gets an containing the keys of the . - /// - /// An containing the keys of the . - public ICollection Keys => _backingDictionary.Keys; - - /// - /// Gets an containing the values in the . - /// - /// An containing the values in the . - public ICollection Values => _backingDictionary.Values; - - /// - /// Adds an item to the . - /// - /// The object to add to the . - public void Add(KeyValuePair item) => this[item.Key] = item.Value; - - /// - /// Removes all items from the . - /// - public void Clear() => _backingDictionary.Clear(); - - /// - /// Determines whether the contains a specific value. - /// - /// - /// if is found in the ; otherwise, . - /// - /// The object to locate in the . - public bool Contains(KeyValuePair item) => _backingDictionary.Contains(item); - - /// - /// Copies the elements of the to an , starting at a particular - /// index. - /// - /// - /// The one-dimensional that is the destination of the elements copied from the - /// . The must have zero-based indexing. - /// - /// The zero-based index in at which copying begins. - public void CopyTo(KeyValuePair[] array, int arrayIndex) => _backingDictionary.CopyTo(array, arrayIndex); - - /// - /// Removes the first occurrence of a specific object from the . - /// - /// - /// if was successfully removed from the ; otherwise, - /// . - /// - /// The object to remove from the . - public bool Remove(KeyValuePair item) => _backingDictionary.Remove(item); - - /// - /// Adds an element with the provided key and value to the . - /// - /// The object to use as the key of the element to add. - /// The object to use as the value of the element to add. - public void Add(string key, DynamicValue value) => this[key] = value; - - /// - /// Determines whether the contains an element with the specified key. - /// - /// - /// if the contains an element with the key; otherwise, . - /// - /// The key to locate in the . - public bool ContainsKey(string key) => _backingDictionary.ContainsKey(key); - - /// - /// Removes the element with the specified key from the . - /// - /// if the element is successfully removed; otherwise, . - /// The key of the element to remove. - public bool Remove(string key) - { - key = GetNeutralKey(key); - return _backingDictionary.Remove(key); + return member; } - - /// - /// Gets the value associated with the specified key. - /// - /// - /// if the contains an element with the specified key; otherwise, - /// . - /// - /// The key whose value to get. - /// - /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default - /// value for the type of the parameter. This parameter is passed uninitialized. - /// - public bool TryGetValue(string key, out DynamicValue value) + set { - if (_backingDictionary.TryGetValue(key, out value)) return true; + name = GetNeutralKey(name); - return false; + _backingDictionary[name] = value ?? DynamicValue.NullValue; } + } - /// - /// Returns the enumeration of all dynamic member names. - /// - /// A that contains dynamic member names. - IEnumerator IEnumerable.GetEnumerator() => _backingDictionary.GetEnumerator(); - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// A that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() => _backingDictionary.GetEnumerator(); - - /// - /// Indicates whether the current is equal to another object of the same type. - /// - /// - /// if the current instance is equal to the parameter; otherwise, - /// . - /// - /// An instance to compare with this instance. - public bool Equals(DynamicDictionary other) - { - if (ReferenceEquals(null, other)) - { - return false; - } + /// + /// Gets an containing the keys of the . + /// + /// An containing the keys of the . + public ICollection Keys => _backingDictionary.Keys; - return ReferenceEquals(this, other) || Equals(other._backingDictionary, _backingDictionary); - } + /// + /// Gets an containing the values in the . + /// + /// An containing the values in the . + public ICollection Values => _backingDictionary.Values; - /// - /// Creates a dynamic dictionary from an instance. - /// - /// An instance, that the dynamic dictionary should be created from. - /// An instance. - public static DynamicDictionary Create(IDictionary values) - { - var instance = new DynamicDictionary(); + /// + /// Adds an item to the . + /// + /// The object to add to the . + public void Add(KeyValuePair item) => this[item.Key] = item.Value; - foreach (var key in values.Keys) - { - var v = values[key]; - instance[key] = v is DynamicValue av ? av : new DynamicValue(v); - } + /// + /// Removes all items from the . + /// + public void Clear() => _backingDictionary.Clear(); - return instance; - } - /// - /// Creates a dynamic dictionary from an instance. - /// - public static DynamicDictionary Create(JsonElement e) + /// + /// Determines whether the contains a specific value. + /// + /// + /// if is found in the ; otherwise, . + /// + /// The object to locate in the . + public bool Contains(KeyValuePair item) => _backingDictionary.Contains(item); + + /// + /// Copies the elements of the to an , starting at a particular + /// index. + /// + /// + /// The one-dimensional that is the destination of the elements copied from the + /// . The must have zero-based indexing. + /// + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _backingDictionary.CopyTo(array, arrayIndex); + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// + /// if was successfully removed from the ; otherwise, + /// . + /// + /// The object to remove from the . + public bool Remove(KeyValuePair item) => _backingDictionary.Remove(item); + + /// + /// Adds an element with the provided key and value to the . + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + public void Add(string key, DynamicValue value) => this[key] = value; + + /// + /// Determines whether the contains an element with the specified key. + /// + /// + /// if the contains an element with the key; otherwise, . + /// + /// The key to locate in the . + public bool ContainsKey(string key) => _backingDictionary.ContainsKey(key); + + /// + /// Removes the element with the specified key from the . + /// + /// if the element is successfully removed; otherwise, . + /// The key of the element to remove. + public bool Remove(string key) + { + key = GetNeutralKey(key); + return _backingDictionary.Remove(key); + } + + /// + /// Gets the value associated with the specified key. + /// + /// + /// if the contains an element with the specified key; otherwise, + /// . + /// + /// The key whose value to get. + /// + /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default + /// value for the type of the parameter. This parameter is passed uninitialized. + /// + public bool TryGetValue(string key, out DynamicValue value) + { + if (_backingDictionary.TryGetValue(key, out value)) return true; + + return false; + } + + /// + /// Returns the enumeration of all dynamic member names. + /// + /// A that contains dynamic member names. + IEnumerator IEnumerable.GetEnumerator() => _backingDictionary.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// A that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() => _backingDictionary.GetEnumerator(); + + /// + /// Indicates whether the current is equal to another object of the same type. + /// + /// + /// if the current instance is equal to the parameter; otherwise, + /// . + /// + /// An instance to compare with this instance. + public bool Equals(DynamicDictionary other) + { + if (ReferenceEquals(null, other)) { - var dict = e.ToDictionary(); - return dict == null ? Empty : Create(dict); + return false; } - /// - /// Provides the implementation for operations that set member values. Classes derived from the - /// class can override this method to specify dynamic behavior for operations such as setting a value for a property. - /// - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language - /// determines the behavior. (In most cases, a language-specific run-time exception is thrown.) - /// - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of - /// the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is - /// an instance of the class derived from the class, binder.Name returns "SampleProperty". The - /// binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// - /// - /// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an - /// instance of the class derived from the class, the is "Test". - /// - public override bool TrySetMember(SetMemberBinder binder, object value) + return ReferenceEquals(this, other) || Equals(other._backingDictionary, _backingDictionary); + } + + /// + /// Creates a dynamic dictionary from an instance. + /// + /// An instance, that the dynamic dictionary should be created from. + /// An instance. + public static DynamicDictionary Create(IDictionary values) + { + var instance = new DynamicDictionary(); + + foreach (var key in values.Keys) { - this[binder.Name] = new DynamicValue(value); - return true; + var v = values[key]; + instance[key] = v is DynamicValue av ? av : new DynamicValue(v); } - /// - /// Provides the implementation for operations that get member values. Classes derived from the - /// class can override this method to specify dynamic behavior for operations such as getting a value for a property. - /// - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language - /// determines the behavior. (In most cases, a run-time exception is thrown.) - /// - /// - /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of - /// the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, - /// where sampleObject is an instance of the class derived from the class, binder.Name returns - /// "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. - /// - /// - /// The result of the get operation. For example, if the method is called for a property, you can assign the property - /// value to . - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - if (!_backingDictionary.TryGetValue(binder.Name, out var v)) - { - result = new DynamicValue(null); - } - else result = v; + return instance; + } + /// + /// Creates a dynamic dictionary from an instance. + /// + public static DynamicDictionary Create(JsonElement e) + { + var dict = e.ToDictionary(); + return dict == null ? Empty : Create(dict); + } - return true; - } + /// + /// Provides the implementation for operations that set member values. Classes derived from the + /// class can override this method to specify dynamic behavior for operations such as setting a value for a property. + /// + /// + /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language + /// determines the behavior. (In most cases, a language-specific run-time exception is thrown.) + /// + /// + /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of + /// the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is + /// an instance of the class derived from the class, binder.Name returns "SampleProperty". The + /// binder.IgnoreCase property specifies whether the member name is case-sensitive. + /// + /// + /// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an + /// instance of the class derived from the class, the is "Test". + /// + public override bool TrySetMember(SetMemberBinder binder, object value) + { + this[binder.Name] = new DynamicValue(value); + return true; + } - /// - /// Returns the enumeration of all dynamic member names. - /// - /// A that contains dynamic member names. - public override IEnumerable GetDynamicMemberNames() => _backingDictionary.Keys; - - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// if the specified is equal to this instance; otherwise, - /// . - /// - public override bool Equals(object obj) + /// + /// Provides the implementation for operations that get member values. Classes derived from the + /// class can override this method to specify dynamic behavior for operations such as getting a value for a property. + /// + /// + /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language + /// determines the behavior. (In most cases, a run-time exception is thrown.) + /// + /// + /// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of + /// the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement, + /// where sampleObject is an instance of the class derived from the class, binder.Name returns + /// "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive. + /// + /// + /// The result of the get operation. For example, if the method is called for a property, you can assign the property + /// value to . + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (!_backingDictionary.TryGetValue(binder.Name, out var v)) { - if (ReferenceEquals(null, obj)) - { - return false; - } + result = new DynamicValue(null); + } + else result = v; - if (ReferenceEquals(this, obj)) - { - return true; - } + return true; + } + + /// + /// Returns the enumeration of all dynamic member names. + /// + /// A that contains dynamic member names. + public override IEnumerable GetDynamicMemberNames() => _backingDictionary.Keys; - return obj.GetType() == typeof(DynamicDictionary) && Equals((DynamicDictionary)obj); + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// + /// if the specified is equal to this instance; otherwise, + /// . + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; } - /// - /// Returns a hash code for this . - /// - /// A hash code for this , suitable for use in hashing algorithms and data structures like a hash table. - public override int GetHashCode() => _backingDictionary?.GetHashCode() ?? 0; + if (ReferenceEquals(this, obj)) + { + return true; + } - private static string GetNeutralKey(string key) => key; + return obj.GetType() == typeof(DynamicDictionary) && Equals((DynamicDictionary)obj); } + + /// + /// Returns a hash code for this . + /// + /// A hash code for this , suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() => _backingDictionary?.GetHashCode() ?? 0; + + private static string GetNeutralKey(string key) => key; } diff --git a/src/Elastic.Transport/Responses/Dynamic/DynamicResponse.cs b/src/Elastic.Transport/Responses/Dynamic/DynamicResponse.cs index 39586a9..6ff2a73 100644 --- a/src/Elastic.Transport/Responses/Dynamic/DynamicResponse.cs +++ b/src/Elastic.Transport/Responses/Dynamic/DynamicResponse.cs @@ -4,39 +4,38 @@ using System.Dynamic; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A type of response that makes it easier to work with responses in an untyped fashion. +/// +/// It exposes the body as which is `dynamic` through +/// +/// Since `dynamic` can be scary in .NET this response also exposes a safe traversal mechanism under +/// which support an xpath'esque syntax to fish for values in the returned json. +/// +/// +public sealed class DynamicResponse : TransportResponse { - /// - /// A type of response that makes it easier to work with responses in an untyped fashion. - /// - /// It exposes the body as which is `dynamic` through - /// - /// Since `dynamic` can be scary in .NET this response also exposes a safe traversal mechanism under - /// which support an xpath'esque syntax to fish for values in the returned json. - /// - /// - public sealed class DynamicResponse : TransportResponse - { - /// - public DynamicResponse() { } + /// + public DynamicResponse() { } - /// - public DynamicResponse(DynamicDictionary dictionary) - { - Body = dictionary; - Dictionary = dictionary; - } + /// + public DynamicResponse(DynamicDictionary dictionary) + { + Body = dictionary; + Dictionary = dictionary; + } - private DynamicDictionary Dictionary { get; } + private DynamicDictionary Dictionary { get; } - /// - /// Traverses data using path notation. - /// e.g some.deep.nested.json.path - /// A special lookup is available for ANY key using _arbitrary_key_ e.g some.deep._arbitrary_key_.json.path which will traverse into the first key - /// - /// path into the stored object, keys are separated with a dot and the last key is returned as T - /// - /// T or default - public T Get(string path) => Dictionary.Get(path); - } + /// + /// Traverses data using path notation. + /// e.g some.deep.nested.json.path + /// A special lookup is available for ANY key using _arbitrary_key_ e.g some.deep._arbitrary_key_.json.path which will traverse into the first key + /// + /// path into the stored object, keys are separated with a dot and the last key is returned as T + /// + /// T or default + public T Get(string path) => Dictionary.Get(path); } diff --git a/src/Elastic.Transport/Responses/Dynamic/DynamicValue.cs b/src/Elastic.Transport/Responses/Dynamic/DynamicValue.cs index 1961cfa..8fda591 100644 --- a/src/Elastic.Transport/Responses/Dynamic/DynamicValue.cs +++ b/src/Elastic.Transport/Responses/Dynamic/DynamicValue.cs @@ -21,968 +21,967 @@ // ReSharper disable RemoveRedundantBraces // ReSharper disable ArrangeMethodOrOperatorBody -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Represents a value returned by . +/// +/// This is `dynamic` through and supports direct casting various valuetypes +/// Since `dynamic` can be scary in .NET this response also exposes a safe traversal mechanism under +/// which support an xpath'esque syntax to fish for values in the returned json. +/// +/// +public sealed class DynamicValue : DynamicObject, IEquatable, IConvertible, IReadOnlyCollection { + private readonly object _value; + + /// + /// Initializes a new instance of the class. + /// + /// The value to store in the instance + public DynamicValue(object value) => _value = value is DynamicValue av ? av.Value : value; + + /// + /// Gets a value indicating whether this instance has value. + /// + /// true if this instance has value; otherwise, false. + /// is considered as not being a value. + public bool HasValue => _value != null; + /// - /// Represents a value returned by . - /// - /// This is `dynamic` through and supports direct casting various valuetypes - /// Since `dynamic` can be scary in .NET this response also exposes a safe traversal mechanism under - /// which support an xpath'esque syntax to fish for values in the returned json. - /// + /// Try to get key from the object this instance references /// - public class DynamicValue : DynamicObject, IEquatable, IConvertible, IReadOnlyCollection + /// + public DynamicValue this[string name] { - private readonly object _value; - - /// - /// Initializes a new instance of the class. - /// - /// The value to store in the instance - public DynamicValue(object value) => _value = value is DynamicValue av ? av.Value : value; - - /// - /// Gets a value indicating whether this instance has value. - /// - /// true if this instance has value; otherwise, false. - /// is considered as not being a value. - public bool HasValue => _value != null; - - /// - /// Try to get key from the object this instance references - /// - /// - public DynamicValue this[string name] - { - get - { - Dispatch(out var r, name); - return (DynamicValue)r; - } + get + { + Dispatch(out var r, name); + return (DynamicValue)r; } + } - /// - public T Get(string path) + /// + public T Get(string path) + { + var dynamicDictionary = Value switch { - var dynamicDictionary = Value switch - { - DynamicDictionary v => v, - IDictionary v => DynamicDictionary.Create(v), - JsonElement e => DynamicDictionary.Create(e), - _ => null - }; - return dynamicDictionary == null ? default : dynamicDictionary.Get(path); - } + DynamicDictionary v => v, + IDictionary v => DynamicDictionary.Create(v), + JsonElement e => DynamicDictionary.Create(e), + _ => null + }; + return dynamicDictionary == null ? default : dynamicDictionary.Get(path); + } - /// - /// A static reusable reference to a holding `null` that is still safe to traverse - /// on through - /// - public static DynamicValue NullValue { get; } = new DynamicValue(null); - - /// - /// Wrap as a if is not wrapped already - /// - /// - /// - public static DynamicValue SelfOrNew(object v) => v is DynamicValue av ? av : new DynamicValue(v); - - /// - /// Gets the inner value - /// - public object Value => _value; - - /// - /// Returns the for this instance. - /// - /// - /// The enumerated constant that is the of the class or value type that implements this interface. - /// - /// 2 - public TypeCode GetTypeCode() - { - if (_value == null) return TypeCode.Empty; - - return Type.GetTypeCode(_value.GetType()); - } + /// + /// A static reusable reference to a holding `null` that is still safe to traverse + /// on through + /// + public static DynamicValue NullValue { get; } = new DynamicValue(null); - /// - /// Converts the value of this instance to an equivalent Boolean value using the specified culture-specific formatting information. - /// - /// - /// A Boolean value equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public bool ToBoolean(IFormatProvider provider) => Convert.ToBoolean(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 8-bit unsigned integer using the specified culture-specific formatting information. - /// - /// - /// An 8-bit unsigned integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public byte ToByte(IFormatProvider provider) => Convert.ToByte(_value, provider); - - /// - /// Converts the value of this instance to an equivalent Unicode character using the specified culture-specific formatting information. - /// - /// - /// A Unicode character equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public char ToChar(IFormatProvider provider) => Convert.ToChar(_value, provider); - - /// - /// Converts the value of this instance to an equivalent using the specified culture-specific formatting - /// information. - /// - /// - /// A instance equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public DateTime ToDateTime(IFormatProvider provider) => Convert.ToDateTime(_value, provider); - - /// - /// Converts the value of this instance to an equivalent number using the specified culture-specific formatting - /// information. - /// - /// - /// A number equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public decimal ToDecimal(IFormatProvider provider) => Convert.ToDecimal(_value, provider); - - /// - /// Converts the value of this instance to an equivalent double-precision floating-point number using the specified culture-specific formatting - /// information. - /// - /// - /// A double-precision floating-point number equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public double ToDouble(IFormatProvider provider) => Convert.ToDouble(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 16-bit signed integer using the specified culture-specific formatting information. - /// - /// - /// An 16-bit signed integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public short ToInt16(IFormatProvider provider) => Convert.ToInt16(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 32-bit signed integer using the specified culture-specific formatting information. - /// - /// - /// An 32-bit signed integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public int ToInt32(IFormatProvider provider) => Convert.ToInt32(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 64-bit signed integer using the specified culture-specific formatting information. - /// - /// - /// An 64-bit signed integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public long ToInt64(IFormatProvider provider) => Convert.ToInt64(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 8-bit signed integer using the specified culture-specific formatting information. - /// - /// - /// An 8-bit signed integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - [CLSCompliant(false)] - public sbyte ToSByte(IFormatProvider provider) => Convert.ToSByte(_value, provider); - - /// - /// Converts the value of this instance to an equivalent single-precision floating-point number using the specified culture-specific formatting - /// information. - /// - /// - /// A single-precision floating-point number equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public float ToSingle(IFormatProvider provider) => Convert.ToSingle(_value, provider); - - /// - /// Converts the value of this instance to an equivalent using the specified culture-specific formatting - /// information. - /// - /// - /// A instance equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public string ToString(IFormatProvider provider) => Convert.ToString(_value, provider)!; - - /// - /// Converts the value of this instance to an of the specified that has an - /// equivalent value, using the specified culture-specific formatting information. - /// - /// - /// An instance of type whose value is equivalent to the value of this - /// instance. - /// - /// The to which the value of this instance is converted. - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - public object ToType(Type conversionType, IFormatProvider provider) => Convert.ChangeType(_value, conversionType, provider); - - /// - /// Converts the value of this instance to an equivalent 16-bit unsigned integer using the specified culture-specific formatting information. - /// - /// - /// An 16-bit unsigned integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - [CLSCompliant(false)] - public ushort ToUInt16(IFormatProvider provider) => Convert.ToUInt16(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 32-bit unsigned integer using the specified culture-specific formatting information. - /// - /// - /// An 32-bit unsigned integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - [CLSCompliant(false)] - public uint ToUInt32(IFormatProvider provider) => Convert.ToUInt32(_value, provider); - - /// - /// Converts the value of this instance to an equivalent 64-bit unsigned integer using the specified culture-specific formatting information. - /// - /// - /// An 64-bit unsigned integer equivalent to the value of this instance. - /// - /// - /// An interface implementation that supplies culture-specific formatting - /// information. - /// - /// 2 - [CLSCompliant(false)] - public ulong ToUInt64(IFormatProvider provider) => Convert.ToUInt64(_value, provider); - - /// - /// Returns the value as a dictionary if the current value represents an object. - /// Otherwise returns null. - /// - public IDictionary ToDictionary() - { - if (_value is IDictionary dict) return DynamicDictionary.Create(dict); - else if (_value is JsonElement e && e.ValueKind == JsonValueKind.Object) - { - var d = e.EnumerateObject() - .Aggregate(new Dictionary(), (dictionary, je) => - { - dictionary.Add(je.Name, je.Value); - return dictionary; - }); - return DynamicDictionary.Create(d); - } + /// + /// Wrap as a if is not wrapped already + /// + /// + /// + public static DynamicValue SelfOrNew(object v) => v is DynamicValue av ? av : new DynamicValue(v); - return null; - } + /// + /// Gets the inner value + /// + public object Value => _value; + /// + /// Returns the for this instance. + /// + /// + /// The enumerated constant that is the of the class or value type that implements this interface. + /// + /// 2 + public TypeCode GetTypeCode() + { + if (_value == null) return TypeCode.Empty; - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// - /// An to compare with this instance. - public bool Equals(DynamicValue compareValue) - { - if (ReferenceEquals(null, compareValue)) - { - return false; - } + return Type.GetTypeCode(_value.GetType()); + } - return ReferenceEquals(this, compareValue) || Equals(compareValue._value, _value); - } + /// + /// Converts the value of this instance to an equivalent Boolean value using the specified culture-specific formatting information. + /// + /// + /// A Boolean value equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public bool ToBoolean(IFormatProvider provider) => Convert.ToBoolean(_value, provider); - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - var name = binder.Name; + /// + /// Converts the value of this instance to an equivalent 8-bit unsigned integer using the specified culture-specific formatting information. + /// + /// + /// An 8-bit unsigned integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public byte ToByte(IFormatProvider provider) => Convert.ToByte(_value, provider); + + /// + /// Converts the value of this instance to an equivalent Unicode character using the specified culture-specific formatting information. + /// + /// + /// A Unicode character equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public char ToChar(IFormatProvider provider) => Convert.ToChar(_value, provider); + + /// + /// Converts the value of this instance to an equivalent using the specified culture-specific formatting + /// information. + /// + /// + /// A instance equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public DateTime ToDateTime(IFormatProvider provider) => Convert.ToDateTime(_value, provider); + + /// + /// Converts the value of this instance to an equivalent number using the specified culture-specific formatting + /// information. + /// + /// + /// A number equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public decimal ToDecimal(IFormatProvider provider) => Convert.ToDecimal(_value, provider); - return Dispatch(out result, name); + /// + /// Converts the value of this instance to an equivalent double-precision floating-point number using the specified culture-specific formatting + /// information. + /// + /// + /// A double-precision floating-point number equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public double ToDouble(IFormatProvider provider) => Convert.ToDouble(_value, provider); + + /// + /// Converts the value of this instance to an equivalent 16-bit signed integer using the specified culture-specific formatting information. + /// + /// + /// An 16-bit signed integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public short ToInt16(IFormatProvider provider) => Convert.ToInt16(_value, provider); + + /// + /// Converts the value of this instance to an equivalent 32-bit signed integer using the specified culture-specific formatting information. + /// + /// + /// An 32-bit signed integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public int ToInt32(IFormatProvider provider) => Convert.ToInt32(_value, provider); + + /// + /// Converts the value of this instance to an equivalent 64-bit signed integer using the specified culture-specific formatting information. + /// + /// + /// An 64-bit signed integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public long ToInt64(IFormatProvider provider) => Convert.ToInt64(_value, provider); + + /// + /// Converts the value of this instance to an equivalent 8-bit signed integer using the specified culture-specific formatting information. + /// + /// + /// An 8-bit signed integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + [CLSCompliant(false)] + public sbyte ToSByte(IFormatProvider provider) => Convert.ToSByte(_value, provider); + + /// + /// Converts the value of this instance to an equivalent single-precision floating-point number using the specified culture-specific formatting + /// information. + /// + /// + /// A single-precision floating-point number equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public float ToSingle(IFormatProvider provider) => Convert.ToSingle(_value, provider); + + /// + /// Converts the value of this instance to an equivalent using the specified culture-specific formatting + /// information. + /// + /// + /// A instance equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public string ToString(IFormatProvider provider) => Convert.ToString(_value, provider)!; + + /// + /// Converts the value of this instance to an of the specified that has an + /// equivalent value, using the specified culture-specific formatting information. + /// + /// + /// An instance of type whose value is equivalent to the value of this + /// instance. + /// + /// The to which the value of this instance is converted. + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + public object ToType(Type conversionType, IFormatProvider provider) => Convert.ChangeType(_value, conversionType, provider); + + /// + /// Converts the value of this instance to an equivalent 16-bit unsigned integer using the specified culture-specific formatting information. + /// + /// + /// An 16-bit unsigned integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + [CLSCompliant(false)] + public ushort ToUInt16(IFormatProvider provider) => Convert.ToUInt16(_value, provider); + + /// + /// Converts the value of this instance to an equivalent 32-bit unsigned integer using the specified culture-specific formatting information. + /// + /// + /// An 32-bit unsigned integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + [CLSCompliant(false)] + public uint ToUInt32(IFormatProvider provider) => Convert.ToUInt32(_value, provider); + + /// + /// Converts the value of this instance to an equivalent 64-bit unsigned integer using the specified culture-specific formatting information. + /// + /// + /// An 64-bit unsigned integer equivalent to the value of this instance. + /// + /// + /// An interface implementation that supplies culture-specific formatting + /// information. + /// + /// 2 + [CLSCompliant(false)] + public ulong ToUInt64(IFormatProvider provider) => Convert.ToUInt64(_value, provider); + + /// + /// Returns the value as a dictionary if the current value represents an object. + /// Otherwise returns null. + /// + public IDictionary ToDictionary() + { + if (_value is IDictionary dict) return DynamicDictionary.Create(dict); + else if (_value is JsonElement e && e.ValueKind == JsonValueKind.Object) + { + var d = e.EnumerateObject() + .Aggregate(new Dictionary(), (dictionary, je) => + { + dictionary.Add(je.Name, je.Value); + return dictionary; + }); + return DynamicDictionary.Create(d); } + return null; + } + - private bool Dispatch(out object result, string name) + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An to compare with this instance. + public bool Equals(DynamicValue compareValue) + { + if (ReferenceEquals(null, compareValue)) { - if (!HasValue) - { - result = NullValue; - return true; - } + return false; + } - if (Value is IDictionary d) - { - result = d.TryGetValue(name, out var r) ? SelfOrNew(r) : NullValue; - return true; - } - if (Value is IDynamicMetaObjectProvider) - { - var dm = GetDynamicMember(Value, name); - result = SelfOrNew(dm); - return true; - } - if (Value is IDictionary ds) - { - result = ds.Contains(name) ? SelfOrNew(ds[name]) : NullValue; - return true; - } - if (Value is IList l) - { - var projected = l - .Cast() - .Select(i => SelfOrNew(i).Dispatch(out var o, name) ? o : null) - .Where(i => i != null) - .ToArray(); - result = SelfOrNew(projected); - return projected.Length > 0; - } - if (Value is IList lo) - { - var projected = lo - .Select(i => SelfOrNew(i).Dispatch(out var o, name) ? o : null) - .Where(i => i != null) - .ToArray(); - result = SelfOrNew(projected); - return projected.Length > 0; - } - if (Value is JsonElement e && e.ValueKind == JsonValueKind.Object) - { - if (e.TryGetProperty(name, out var r)) - { - result = SelfOrNew(r); - return true; - } - } - if (Value is JsonElement a && a.ValueKind == JsonValueKind.Array) - { - var projected = a.EnumerateArray() - .Select(i => SelfOrNew(i).Dispatch(out var o, name) ? o : null) - .Where(i => i != null) - .ToArray(); - result = SelfOrNew(projected); - return projected.Length > 0; - } + return ReferenceEquals(this, compareValue) || Equals(compareValue._value, _value); + } + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var name = binder.Name; + + return Dispatch(out result, name); + } + + + private bool Dispatch(out object result, string name) + { + if (!HasValue) + { result = NullValue; return true; } - private static object GetDynamicMember(object obj, string memberName) + if (Value is IDictionary d) { - var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, null, - new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); - var callsite = CallSite>.Create(binder); - return callsite.Target(callsite, obj); + result = d.TryGetValue(name, out var r) ? SelfOrNew(r) : NullValue; + return true; } - - /// - /// Returns a default value if Value is null - /// - /// When no default value is supplied, required to supply the default type - /// Optional parameter for default value, if not given it returns default of type T - /// If value is not null, value is returned, else default value is returned - public T Default(T defaultValue = default(T)) + if (Value is IDynamicMetaObjectProvider) + { + var dm = GetDynamicMember(Value, name); + result = SelfOrNew(dm); + return true; + } + if (Value is IDictionary ds) + { + result = ds.Contains(name) ? SelfOrNew(ds[name]) : NullValue; + return true; + } + if (Value is IList l) { - if (HasValue) + var projected = l + .Cast() + .Select(i => SelfOrNew(i).Dispatch(out var o, name) ? o : null) + .Where(i => i != null) + .ToArray(); + result = SelfOrNew(projected); + return projected.Length > 0; + } + if (Value is IList lo) + { + var projected = lo + .Select(i => SelfOrNew(i).Dispatch(out var o, name) ? o : null) + .Where(i => i != null) + .ToArray(); + result = SelfOrNew(projected); + return projected.Length > 0; + } + if (Value is JsonElement e && e.ValueKind == JsonValueKind.Object) + { + if (e.TryGetProperty(name, out var r)) { - try - { - return (T)_value; - } - catch - { - var typeName = _value.GetType().Name; - var message = string.Format("Cannot convert value of type '{0}' to type '{1}'", - typeName, typeof(T).Name); - - throw new InvalidCastException(message); - } + result = SelfOrNew(r); + return true; } - - return defaultValue; } - - /// - /// Attempts to convert the value to type of T, failing to do so will return the defaultValue. - /// - /// When no default value is supplied, required to supply the default type - /// Optional parameter for default value, if not given it returns default of type T - /// If value is not null, value is returned, else default value is returned - public T TryParse(T defaultValue = default) + if (Value is JsonElement a && a.ValueKind == JsonValueKind.Array) { - if (!HasValue) return defaultValue; + var projected = a.EnumerateArray() + .Select(i => SelfOrNew(i).Dispatch(out var o, name) ? o : null) + .Where(i => i != null) + .ToArray(); + result = SelfOrNew(projected); + return projected.Length > 0; + } + + result = NullValue; + return true; + } + + private static object GetDynamicMember(object obj, string memberName) + { + var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, null, + new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); + var callsite = CallSite>.Create(binder); + return callsite.Target(callsite, obj); + } + /// + /// Returns a default value if Value is null + /// + /// When no default value is supplied, required to supply the default type + /// Optional parameter for default value, if not given it returns default of type T + /// If value is not null, value is returned, else default value is returned + public T Default(T defaultValue = default(T)) + { + if (HasValue) + { try { - return TryParse(defaultValue, typeof(T), _value, out var o) ? (T)o : defaultValue; + return (T)_value; } catch { - return defaultValue; + var typeName = _value.GetType().Name; + var message = string.Format("Cannot convert value of type '{0}' to type '{1}'", + typeName, typeof(T).Name); + + throw new InvalidCastException(message); } } -#pragma warning disable 1591 - public static object ConsumeJsonElement(Type targetReturnType, JsonElement e) + return defaultValue; + } + + /// + /// Attempts to convert the value to type of T, failing to do so will return the defaultValue. + /// + /// When no default value is supplied, required to supply the default type + /// Optional parameter for default value, if not given it returns default of type T + /// If value is not null, value is returned, else default value is returned + public T TryParse(T defaultValue = default) + { + if (!HasValue) return defaultValue; + + try { - // ReSharper disable once HeapView.BoxingAllocation - object ParseNumber(JsonElement el) - { - if (el.TryGetInt64(out var l)) - return l; + return TryParse(defaultValue, typeof(T), _value, out var o) ? (T)o : defaultValue; + } + catch + { + return defaultValue; + } + } - return el.GetDouble(); - } +#pragma warning disable 1591 + public static object ConsumeJsonElement(Type targetReturnType, JsonElement e) + { + // ReSharper disable once HeapView.BoxingAllocation + object ParseNumber(JsonElement el) + { + if (el.TryGetInt64(out var l)) + return l; - return targetReturnType switch - { - _ when targetReturnType == typeof(bool) => e.GetBoolean(), - _ when targetReturnType == typeof(byte) => e.GetByte(), - _ when targetReturnType == typeof(decimal) => e.GetDecimal(), - _ when targetReturnType == typeof(double) => e.GetDouble(), - _ when targetReturnType == typeof(Guid) => e.GetGuid(), - _ when targetReturnType == typeof(short) => e.GetInt16(), - _ when targetReturnType == typeof(int) => e.GetInt32(), - _ when targetReturnType == typeof(long) => e.GetInt64(), - _ when targetReturnType == typeof(float) => e.GetSingle(), - _ when targetReturnType == typeof(string) => e.GetString(), - _ when targetReturnType == typeof(DateTime) => e.GetDateTime(), - _ when targetReturnType == typeof(DateTimeOffset) => e.GetDateTimeOffset(), - _ when targetReturnType == typeof(ushort) => e.GetUInt16(), - _ when targetReturnType == typeof(uint) => e.GetUInt32(), - _ when targetReturnType == typeof(ulong) => e.GetUInt64(), - _ when targetReturnType == typeof(sbyte) => e.GetSByte(), - _ when targetReturnType == typeof(DynamicDictionary) => DynamicDictionary.Create(e), - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Array => - e.EnumerateArray().Select(je => ConsumeJsonElement(targetReturnType, je)).ToArray(), - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Object => e.ToDictionary(), - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.True => true, - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.False => false, - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Null => null, - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.String => e.GetString(), - _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Number => ParseNumber(e), - _ => null - }; + return el.GetDouble(); } - internal bool TryParse(object defaultValue, Type targetReturnType, object value, out object newObject) + return targetReturnType switch { - newObject = defaultValue; - if (value == null) return false; + _ when targetReturnType == typeof(bool) => e.GetBoolean(), + _ when targetReturnType == typeof(byte) => e.GetByte(), + _ when targetReturnType == typeof(decimal) => e.GetDecimal(), + _ when targetReturnType == typeof(double) => e.GetDouble(), + _ when targetReturnType == typeof(Guid) => e.GetGuid(), + _ when targetReturnType == typeof(short) => e.GetInt16(), + _ when targetReturnType == typeof(int) => e.GetInt32(), + _ when targetReturnType == typeof(long) => e.GetInt64(), + _ when targetReturnType == typeof(float) => e.GetSingle(), + _ when targetReturnType == typeof(string) => e.GetString(), + _ when targetReturnType == typeof(DateTime) => e.GetDateTime(), + _ when targetReturnType == typeof(DateTimeOffset) => e.GetDateTimeOffset(), + _ when targetReturnType == typeof(ushort) => e.GetUInt16(), + _ when targetReturnType == typeof(uint) => e.GetUInt32(), + _ when targetReturnType == typeof(ulong) => e.GetUInt64(), + _ when targetReturnType == typeof(sbyte) => e.GetSByte(), + _ when targetReturnType == typeof(DynamicDictionary) => DynamicDictionary.Create(e), + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Array => + e.EnumerateArray().Select(je => ConsumeJsonElement(targetReturnType, je)).ToArray(), + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Object => e.ToDictionary(), + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.True => true, + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.False => false, + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Null => null, + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.String => e.GetString(), + _ when targetReturnType == typeof(object) && e.ValueKind == JsonValueKind.Number => ParseNumber(e), + _ => null + }; + } - if (targetReturnType.IsGenericType && targetReturnType.GetGenericTypeDefinition() == typeof(Nullable<>)) - targetReturnType = targetReturnType.GenericTypeArguments[0]; + internal bool TryParse(object defaultValue, Type targetReturnType, object value, out object newObject) + { + newObject = defaultValue; + if (value == null) return false; - try + if (targetReturnType.IsGenericType && targetReturnType.GetGenericTypeDefinition() == typeof(Nullable<>)) + targetReturnType = targetReturnType.GenericTypeArguments[0]; + + try + { + if (value is JsonElement e) { - if (value is JsonElement e) - { - // ReSharper disable once HeapView.BoxingAllocation - newObject = ConsumeJsonElement(targetReturnType, e); - return true; - } + // ReSharper disable once HeapView.BoxingAllocation + newObject = ConsumeJsonElement(targetReturnType, e); + return true; + } - var valueType = value.GetType(); - if (targetReturnType.IsArray && value is DynamicValue v) - { - value = v.Value; - valueType = value.GetType(); - } - if (targetReturnType.IsArray) + var valueType = value.GetType(); + if (targetReturnType.IsArray && value is DynamicValue v) + { + value = v.Value; + valueType = value.GetType(); + } + if (targetReturnType.IsArray) + { + if (!valueType.IsArray) { - if (!valueType.IsArray) - { - return false; - } - var ar = (object[])value; - var t = targetReturnType.GetElementType(); - var objectArray = ar - .Select(a => TryParse(defaultValue, t, a, out var o) ? o : null) - .Where(a => a != null) - .ToArray(); - - var arr = Array.CreateInstance(t, objectArray.Length); - Array.Copy(objectArray, arr, objectArray.Length); - newObject = arr; - return true; + return false; } + var ar = (object[])value; + var t = targetReturnType.GetElementType(); + var objectArray = ar + .Select(a => TryParse(defaultValue, t, a, out var o) ? o : null) + .Where(a => a != null) + .ToArray(); - if (valueType.IsAssignableFrom(targetReturnType)) - { - newObject = value; - return true; - } + var arr = Array.CreateInstance(t, objectArray.Length); + Array.Copy(objectArray, arr, objectArray.Length); + newObject = arr; + return true; + } - var stringValue = value as string; + if (valueType.IsAssignableFrom(targetReturnType)) + { + newObject = value; + return true; + } - if (targetReturnType == typeof(DateTime) - && DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result)) - { - newObject = result; - return true; - } - if (stringValue != null) - { - if (targetReturnType == typeof(object)) - { - newObject = Convert.ChangeType(value, targetReturnType); - return true; - } - - var converter = TypeDescriptor.GetConverter(targetReturnType); - if (converter.IsValid(stringValue)) - { - newObject = converter.ConvertFromInvariantString(stringValue); - return true; - } - } - else if (value is DynamicValue dv) - return dv.TryParse(defaultValue, targetReturnType, dv.Value, out newObject); - else if (targetReturnType == typeof(string)) - { - newObject = Convert.ChangeType(value, TypeCode.String, CultureInfo.InvariantCulture); - return true; - } - else if (valueType.IsValueType) - { - newObject = Convert.ChangeType(_value, targetReturnType); - return true; - } - else if (targetReturnType == typeof(DynamicDictionary) && valueType == typeof(Dictionary)) + var stringValue = value as string; + + if (targetReturnType == typeof(DateTime) + && DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result)) + { + newObject = result; + return true; + } + if (stringValue != null) + { + if (targetReturnType == typeof(object)) { - newObject = DynamicDictionary.Create(value as Dictionary); + newObject = Convert.ChangeType(value, targetReturnType); return true; } - else if (targetReturnType == typeof(object)) + var converter = TypeDescriptor.GetConverter(targetReturnType); + if (converter.IsValid(stringValue)) { - newObject = value; + newObject = converter.ConvertFromInvariantString(stringValue); return true; } } - catch + else if (value is DynamicValue dv) + return dv.TryParse(defaultValue, targetReturnType, dv.Value, out newObject); + else if (targetReturnType == typeof(string)) { - return false; + newObject = Convert.ChangeType(value, TypeCode.String, CultureInfo.InvariantCulture); + return true; } + else if (valueType.IsValueType) + { + newObject = Convert.ChangeType(_value, targetReturnType); + return true; + } + else if (targetReturnType == typeof(DynamicDictionary) && valueType == typeof(Dictionary)) + { + newObject = DynamicDictionary.Create(value as Dictionary); + return true; + } + + else if (targetReturnType == typeof(object)) + { + newObject = value; + return true; + } + } + catch + { return false; } + return false; + } - public static bool operator ==(DynamicValue dynamicValue, object compareValue) + public static bool operator ==(DynamicValue dynamicValue, object compareValue) + { + if (dynamicValue._value == null && compareValue == null) { - if (dynamicValue._value == null && compareValue == null) - { - return true; - } - - return dynamicValue._value != null && dynamicValue._value.Equals(compareValue); + return true; } - public static bool operator !=(DynamicValue dynamicValue, object compareValue) => !(dynamicValue == compareValue); + return dynamicValue._value != null && dynamicValue._value.Equals(compareValue); + } + + public static bool operator !=(DynamicValue dynamicValue, object compareValue) => !(dynamicValue == compareValue); - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified is equal to the current ; otherwise, - /// false. - /// - /// The to compare with the current . - public override bool Equals(object compareValue) + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// true if the specified is equal to the current ; otherwise, + /// false. + /// + /// The to compare with the current . + public override bool Equals(object compareValue) + { + if (ReferenceEquals(null, compareValue)) { - if (ReferenceEquals(null, compareValue)) - { - return false; - } + return false; + } - if (ReferenceEquals(this, compareValue) - || ReferenceEquals(_value, compareValue) - || Equals(_value, compareValue) - ) - { - return true; - } + if (ReferenceEquals(this, compareValue) + || ReferenceEquals(_value, compareValue) + || Equals(_value, compareValue) + ) + { + return true; + } + + return compareValue.GetType() == typeof(DynamicValue) && Equals((DynamicValue)compareValue); + } - return compareValue.GetType() == typeof(DynamicValue) && Equals((DynamicValue)compareValue); + /// + /// Serves as a hash function for a particular type. + /// + /// A hash code for the current instance. + public override int GetHashCode() => _value != null ? _value.GetHashCode() : 0; + + /// + /// Provides implementation for binary operations. Classes derived from the class can override + /// this method to specify dynamic behavior for operations such as addition and multiplication. + /// + /// + /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of + /// the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) + /// + /// + /// Provides information about the binary operation. The binder.Operation property returns an + /// object. For example, for the sum = first + second statement, where first and second + /// are derived from the DynamicObject class, binder.Operation returns ExpressionType.Add. + /// + /// + /// The right operand for the binary operation. For example, for the sum = first + second statement, where first and second + /// are derived from the DynamicObject class, is equal to second. + /// + /// The result of the binary operation. + public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) + { + result = null; + + if (binder.Operation != ExpressionType.Equal) + { + return false; } - /// - /// Serves as a hash function for a particular type. - /// - /// A hash code for the current instance. - public override int GetHashCode() => _value != null ? _value.GetHashCode() : 0; - - /// - /// Provides implementation for binary operations. Classes derived from the class can override - /// this method to specify dynamic behavior for operations such as addition and multiplication. - /// - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of - /// the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) - /// - /// - /// Provides information about the binary operation. The binder.Operation property returns an - /// object. For example, for the sum = first + second statement, where first and second - /// are derived from the DynamicObject class, binder.Operation returns ExpressionType.Add. - /// - /// - /// The right operand for the binary operation. For example, for the sum = first + second statement, where first and second - /// are derived from the DynamicObject class, is equal to second. - /// - /// The result of the binary operation. - public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) - { - result = null; - - if (binder.Operation != ExpressionType.Equal) - { - return false; - } + var convert = + Binder.Convert(CSharpBinderFlags.None, arg.GetType(), typeof(DynamicValue)); + + if (!TryConvert((ConvertBinder)convert, out var resultOfCast)) + { + return false; + } - var convert = - Binder.Convert(CSharpBinderFlags.None, arg.GetType(), typeof(DynamicValue)); + result = resultOfCast == null ? Equals(arg, resultOfCast) : resultOfCast.Equals(arg); - if (!TryConvert((ConvertBinder)convert, out var resultOfCast)) - { - return false; - } + return true; + } - result = resultOfCast == null ? Equals(arg, resultOfCast) : resultOfCast.Equals(arg); + /// + /// Provides implementation for type conversion operations. Classes derived from the class can + /// override this method to specify dynamic behavior for operations that convert an object from one type to another. + /// + /// + /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of + /// the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) + /// + /// + /// Provides information about the conversion operation. The binder.Type property provides the type to which the object + /// must be converted. For example, for the statement (String)sampleObject in C# (CType(sampleObject, Type) in Visual Basic), where + /// sampleObject is an instance of the class derived from the class, binder.Type returns the + /// type. The binder.Explicit property provides information about the kind of conversion that occurs. It returns + /// true for explicit conversion and false for implicit conversion. + /// + /// The result of the type conversion operation. + public override bool TryConvert(ConvertBinder binder, out object result) + { + result = null; + if (_value == null) + { return true; } - /// - /// Provides implementation for type conversion operations. Classes derived from the class can - /// override this method to specify dynamic behavior for operations that convert an object from one type to another. - /// - /// - /// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of - /// the language determines the behavior. (In most cases, a language-specific run-time exception is thrown.) - /// - /// - /// Provides information about the conversion operation. The binder.Type property provides the type to which the object - /// must be converted. For example, for the statement (String)sampleObject in C# (CType(sampleObject, Type) in Visual Basic), where - /// sampleObject is an instance of the class derived from the class, binder.Type returns the - /// type. The binder.Explicit property provides information about the kind of conversion that occurs. It returns - /// true for explicit conversion and false for implicit conversion. - /// - /// The result of the type conversion operation. - public override bool TryConvert(ConvertBinder binder, out object result) - { - result = null; - - if (_value == null) - { - return true; - } + var binderType = binder.Type; + + if (binderType == typeof(List)) + { + result = ToEnumerable().ToList(); + return true; + } + if (binderType == typeof(List)) + { + result = ToEnumerable().Cast().ToList(); + return true; + } - var binderType = binder.Type; + if (binderType == typeof(string)) + { + result = Convert.ToString(_value); + return true; + } - if (binderType == typeof(List)) + if (binderType == typeof(Guid) || binderType == typeof(Guid?)) + { + if (Guid.TryParse(Convert.ToString(_value), out var guid)) { - result = ToEnumerable().ToList(); + result = guid; return true; } - if (binderType == typeof(List)) + } + else if (binderType == typeof(TimeSpan) || binderType == typeof(TimeSpan?)) + { + if (TimeSpan.TryParse(Convert.ToString(_value), out var timespan)) { - result = ToEnumerable().Cast().ToList(); + result = timespan; return true; } + } + else + { + if (binderType.IsGenericType && binderType.GetGenericTypeDefinition() == typeof(Nullable<>)) + binderType = binderType.GetGenericArguments()[0]; - if (binderType == typeof(string)) - { - result = Convert.ToString(_value); - return true; - } + var typeCode = Type.GetTypeCode(binderType); - if (binderType == typeof(Guid) || binderType == typeof(Guid?)) - { - if (Guid.TryParse(Convert.ToString(_value), out var guid)) - { - result = guid; - return true; - } - } - else if (binderType == typeof(TimeSpan) || binderType == typeof(TimeSpan?)) + if (typeCode == TypeCode.Object) { - if (TimeSpan.TryParse(Convert.ToString(_value), out var timespan)) + if (binderType.IsAssignableFrom(_value.GetType())) { - result = timespan; + result = _value; return true; } + else + return false; } - else - { - if (binderType.IsGenericType && binderType.GetGenericTypeDefinition() == typeof(Nullable<>)) - binderType = binderType.GetGenericArguments()[0]; - - var typeCode = Type.GetTypeCode(binderType); - - if (typeCode == TypeCode.Object) - { - if (binderType.IsAssignableFrom(_value.GetType())) - { - result = _value; - return true; - } - else - return false; - } - result = Convert.ChangeType(_value, typeCode); - return true; - } - return base.TryConvert(binder, out result); + result = Convert.ChangeType(_value, typeCode); + return true; } + return base.TryConvert(binder, out result); + } - public override string ToString() => _value == null ? base.ToString() : Convert.ToString(_value); - - public static implicit operator bool(DynamicValue dynamicValue) - { - if (!dynamicValue.HasValue) return false; - if (dynamicValue._value is JsonElement e) return e.GetBoolean(); + public override string ToString() => _value == null ? base.ToString() : Convert.ToString(_value); - if (dynamicValue._value.GetType().IsValueType) return Convert.ToBoolean(dynamicValue._value); + public static implicit operator bool(DynamicValue dynamicValue) + { + if (!dynamicValue.HasValue) return false; + if (dynamicValue._value is JsonElement e) return e.GetBoolean(); - if (bool.TryParse(dynamicValue.ToString(CultureInfo.InvariantCulture), out var result)) return result; + if (dynamicValue._value.GetType().IsValueType) return Convert.ToBoolean(dynamicValue._value); - return true; - } + if (bool.TryParse(dynamicValue.ToString(CultureInfo.InvariantCulture), out var result)) return result; - public static implicit operator string(DynamicValue dynamicValue) - { - if (!dynamicValue.HasValue) return null; - if (dynamicValue._value is JsonElement e) return e.GetString(); + return true; + } - return Convert.ToString(dynamicValue._value); - } + public static implicit operator string(DynamicValue dynamicValue) + { + if (!dynamicValue.HasValue) return null; + if (dynamicValue._value is JsonElement e) return e.GetString(); - public static implicit operator int(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetInt32(out var v)) return v; - if (dynamicValue._value.GetType().IsValueType) return Convert.ToInt32(dynamicValue._value); + return Convert.ToString(dynamicValue._value); + } - return int.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + public static implicit operator int(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetInt32(out var v)) return v; + if (dynamicValue._value.GetType().IsValueType) return Convert.ToInt32(dynamicValue._value); - public static implicit operator Guid(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetGuid(out var v)) return v; - return dynamicValue._value is Guid guid ? guid : Guid.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + return int.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - public static implicit operator DateTime(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetDateTime(out var v)) return v; - return dynamicValue._value is DateTime dateTime - ? dateTime - : DateTime.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + public static implicit operator Guid(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetGuid(out var v)) return v; + return dynamicValue._value is Guid guid ? guid : Guid.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - public static implicit operator DateTimeOffset(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetDateTimeOffset(out var v)) return v; - return dynamicValue._value is DateTimeOffset offset ? offset : DateTimeOffset.Parse(dynamicValue); - } + public static implicit operator DateTime(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetDateTime(out var v)) return v; + return dynamicValue._value is DateTime dateTime + ? dateTime + : DateTime.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - public static implicit operator TimeSpan(DynamicValue dynamicValue) => - dynamicValue._value is TimeSpan timeSpan - ? timeSpan - : TimeSpan.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + public static implicit operator DateTimeOffset(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetDateTimeOffset(out var v)) return v; + return dynamicValue._value is DateTimeOffset offset ? offset : DateTimeOffset.Parse(dynamicValue); + } - public static implicit operator long(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetInt64(out var v)) return v; - if (dynamicValue._value.GetType().IsValueType) return Convert.ToInt64(dynamicValue._value); + public static implicit operator TimeSpan(DynamicValue dynamicValue) => + dynamicValue._value is TimeSpan timeSpan + ? timeSpan + : TimeSpan.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - return long.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + public static implicit operator long(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetInt64(out var v)) return v; + if (dynamicValue._value.GetType().IsValueType) return Convert.ToInt64(dynamicValue._value); - public static implicit operator float(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetSingle(out var v)) return v; - if (dynamicValue._value.GetType().IsValueType) return Convert.ToSingle(dynamicValue._value); + return long.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - return float.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + public static implicit operator float(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetSingle(out var v)) return v; + if (dynamicValue._value.GetType().IsValueType) return Convert.ToSingle(dynamicValue._value); - public static implicit operator decimal(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetDecimal(out var v)) return v; - if (dynamicValue._value.GetType().IsValueType) return Convert.ToDecimal(dynamicValue._value); + return float.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - return decimal.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + public static implicit operator decimal(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetDecimal(out var v)) return v; + if (dynamicValue._value.GetType().IsValueType) return Convert.ToDecimal(dynamicValue._value); - public static implicit operator double(DynamicValue dynamicValue) - { - if (dynamicValue._value is JsonElement e && e.TryGetDouble(out var v)) return v; - if (dynamicValue._value.GetType().IsValueType) return Convert.ToDouble(dynamicValue._value); + return decimal.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - return double.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); - } + public static implicit operator double(DynamicValue dynamicValue) + { + if (dynamicValue._value is JsonElement e && e.TryGetDouble(out var v)) return v; + if (dynamicValue._value.GetType().IsValueType) return Convert.ToDouble(dynamicValue._value); - public IEnumerable ToEnumerable() - { - using var e = GetEnumerator(); - while(e.MoveNext()) - yield return e.Current; - } + return double.Parse(dynamicValue.ToString(CultureInfo.InvariantCulture)); + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerable ToEnumerable() + { + using var e = GetEnumerator(); + while(e.MoveNext()) + yield return e.Current; + } - public IEnumerator GetEnumerator() - { - if (Value is ICollection c) return c.OfType().Select(v => SelfOrNew(v)).GetEnumerator(); - else if (Value is IList l) return l.OfType().Select(v => SelfOrNew(v)).GetEnumerator(); - else if (Value is IDictionary d) return d.Select(kv=> SelfOrNew(kv.Value)).GetEnumerator(); - else if (Value is IDictionary dv) return dv.Values.GetEnumerator(); - else if (Value is JsonElement e && e.ValueKind == JsonValueKind.Array) return e.EnumerateArray().Select(a=> SelfOrNew(a)).GetEnumerator(); - else if (Value is JsonElement el && el.ValueKind == JsonValueKind.Object) - return ToDictionary().Values.Select(v => SelfOrNew(v)).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - return Value == null - ? Enumerable.Empty().GetEnumerator() - : new List() { this }.GetEnumerator(); - } + public IEnumerator GetEnumerator() + { + if (Value is ICollection c) return c.OfType().Select(v => SelfOrNew(v)).GetEnumerator(); + else if (Value is IList l) return l.OfType().Select(v => SelfOrNew(v)).GetEnumerator(); + else if (Value is IDictionary d) return d.Select(kv=> SelfOrNew(kv.Value)).GetEnumerator(); + else if (Value is IDictionary dv) return dv.Values.GetEnumerator(); + else if (Value is JsonElement e && e.ValueKind == JsonValueKind.Array) return e.EnumerateArray().Select(a=> SelfOrNew(a)).GetEnumerator(); + else if (Value is JsonElement el && el.ValueKind == JsonValueKind.Object) + return ToDictionary().Values.Select(v => SelfOrNew(v)).GetEnumerator(); + + return Value == null + ? Enumerable.Empty().GetEnumerator() + : new List() { this }.GetEnumerator(); + } - public int Count + public int Count + { + get { - get - { - if (Value is ICollection c) return c.Count; - else if (Value is IList l) return l.Count; - else if (Value is IDictionary d) return d.Count; - else if (Value is IDictionary dv) return dv.Count; - else if (Value is JsonElement e && e.ValueKind == JsonValueKind.Array) return e.GetArrayLength(); - else if (Value is JsonElement el && el.ValueKind == JsonValueKind.Object) return el.EnumerateObject().Count(); - - return Value == null ? 0 : 1; - } + if (Value is ICollection c) return c.Count; + else if (Value is IList l) return l.Count; + else if (Value is IDictionary d) return d.Count; + else if (Value is IDictionary dv) return dv.Count; + else if (Value is JsonElement e && e.ValueKind == JsonValueKind.Array) return e.GetArrayLength(); + else if (Value is JsonElement el && el.ValueKind == JsonValueKind.Object) return el.EnumerateObject().Count(); + + return Value == null ? 0 : 1; } + } - public DynamicValue this[int i] + public DynamicValue this[int i] + { + get { - get - { - if (!HasValue) return NullValue; - - var v = Value; + if (!HasValue) return NullValue; - if (v is IList l && l.Count - 1 >= i) - return SelfOrNew(l[i]); - if (v is IList o && o.Count - 1 >= i) - return SelfOrNew(o[i]); + var v = Value; - if (v is IDictionary d) - { - if (d.TryGetValue(i.ToString(CultureInfo.InvariantCulture), out v)) - return SelfOrNew(v); + if (v is IList l && l.Count - 1 >= i) + return SelfOrNew(l[i]); + if (v is IList o && o.Count - 1 >= i) + return SelfOrNew(o[i]); - if (i >= d.Count) return new DynamicValue(null); - var at = d[d.Keys.ElementAt(i)]; - return SelfOrNew(at); - } - if (v is IDictionary dv) - { - if (dv.TryGetValue(i.ToString(CultureInfo.InvariantCulture), out var dvv)) - return dvv; + if (v is IDictionary d) + { + if (d.TryGetValue(i.ToString(CultureInfo.InvariantCulture), out v)) + return SelfOrNew(v); - if (i >= dv.Count) return new DynamicValue(null); - var at = dv[dv.Keys.ElementAt(i)]; - return at; - } - if (v is JsonElement e && e.ValueKind == JsonValueKind.Array) - { - if (e.GetArrayLength() -1 >= i) - return SelfOrNew(e[i]); - } - if (v is JsonElement el && el.ValueKind == JsonValueKind.Object) - { - return SelfOrNew(el.GetProperty(i.ToString(CultureInfo.InvariantCulture))); - } + if (i >= d.Count) return new DynamicValue(null); + var at = d[d.Keys.ElementAt(i)]; + return SelfOrNew(at); + } + if (v is IDictionary dv) + { + if (dv.TryGetValue(i.ToString(CultureInfo.InvariantCulture), out var dvv)) + return dvv; - return NullValue; + if (i >= dv.Count) return new DynamicValue(null); + var at = dv[dv.Keys.ElementAt(i)]; + return at; + } + if (v is JsonElement e && e.ValueKind == JsonValueKind.Array) + { + if (e.GetArrayLength() -1 >= i) + return SelfOrNew(e[i]); + } + if (v is JsonElement el && el.ValueKind == JsonValueKind.Object) + { + return SelfOrNew(el.GetProperty(i.ToString(CultureInfo.InvariantCulture))); } + + return NullValue; } + } - } } diff --git a/src/Elastic.Transport/Responses/EmptyError.cs b/src/Elastic.Transport/Responses/EmptyError.cs new file mode 100644 index 0000000..5fad61b --- /dev/null +++ b/src/Elastic.Transport/Responses/EmptyError.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Transport; + +/// +/// +/// +internal sealed class EmptyError : ErrorResponse +{ + public static readonly EmptyError Instance = new(); + + /// + public override bool HasError() => false; +} diff --git a/src/Elastic.Transport/Responses/IErrorResponse.cs b/src/Elastic.Transport/Responses/ErrorResponse.cs similarity index 76% rename from src/Elastic.Transport/Responses/IErrorResponse.cs rename to src/Elastic.Transport/Responses/ErrorResponse.cs index d0b3174..a0af300 100644 --- a/src/Elastic.Transport/Responses/IErrorResponse.cs +++ b/src/Elastic.Transport/Responses/ErrorResponse.cs @@ -9,20 +9,11 @@ namespace Elastic.Transport; /// public abstract class ErrorResponse { + internal ErrorResponse() { } + /// /// May be called by transport to establish whether the instance represents a valid, complete error. /// This may not always be the case if the error is partially deserialised on the response. /// public abstract bool HasError(); } - -/// -/// -/// -internal sealed class EmptyError : ErrorResponse -{ - public static readonly EmptyError Instance = new(); - - /// - public override bool HasError() => false; -} diff --git a/src/Elastic.Transport/Responses/HttpDetails/ApiCallDetails.cs b/src/Elastic.Transport/Responses/HttpDetails/ApiCallDetails.cs index 0f543d0..09d7394 100644 --- a/src/Elastic.Transport/Responses/HttpDetails/ApiCallDetails.cs +++ b/src/Elastic.Transport/Responses/HttpDetails/ApiCallDetails.cs @@ -10,85 +10,129 @@ using Elastic.Transport.Diagnostics.Auditing; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// +/// +public sealed class ApiCallDetails { - /// - public sealed class ApiCallDetails : IApiCallDetails - { - private string _debugInformation; + private string _debugInformation; - /// - public IEnumerable AuditTrail { get; set; } + internal ApiCallDetails() { } - /// - public IReadOnlyDictionary ThreadPoolStats { get; set; } + /// + /// + /// > + public IEnumerable AuditTrail { get; internal set; } - /// - public IReadOnlyDictionary TcpStats { get; set; } + /// + /// + /// + internal IReadOnlyDictionary ThreadPoolStats { get; set; } - /// - public string DebugInformation - { - get - { - if (_debugInformation != null) - return _debugInformation; - - var sb = new StringBuilder(); - sb.AppendLine(ToString()); - _debugInformation = ResponseStatics.DebugInformationBuilder(this, sb); + /// + /// + /// + internal IReadOnlyDictionary TcpStats { get; set; } + /// + /// + /// + public string DebugInformation + { + get + { + if (_debugInformation != null) return _debugInformation; - } - } - - /// - public HttpMethod HttpMethod { get; set; } - - /// - public int? HttpStatusCode { get; set; } - - /// - public Exception OriginalException { get; set; } - /// - public byte[] RequestBodyInBytes { get; set; } + var sb = new StringBuilder(); + sb.AppendLine(ToString()); + _debugInformation = ResponseStatics.DebugInformationBuilder(this, sb); - /// - public byte[] ResponseBodyInBytes { get; set; } - - /// - public string ResponseMimeType { get; set; } - - /// - public bool Success { get; set; } - - /// - public bool SuccessOrKnownError => - Success || HttpStatusCode >= 400 - && HttpStatusCode < 599 - && HttpStatusCode != 504 //Gateway timeout needs to be retried - && HttpStatusCode != 503 //service unavailable needs to be retried - && HttpStatusCode != 502; - - /// - public Uri Uri { get; set; } - - /// - public ITransportConfiguration TransportConfiguration { get; set; } - - /// - public IReadOnlyDictionary> ParsedHeaders { get; set; } - - /// - /// The error response is the server returned JSON describing a server error. - /// - public ErrorResponse ErrorResponse { get; set; } = EmptyError.Instance; - - /// - /// A string summarising the API call. - /// - public override string ToString() => - $"{(Success ? "S" : "Uns")}uccessful ({HttpStatusCode}) low level call on {HttpMethod.GetStringValue()}: {Uri.PathAndQuery}"; + return _debugInformation; + } } + + /// + /// + /// + public HttpMethod HttpMethod { get; internal set; } + + /// + /// + /// + public int? HttpStatusCode { get; internal set; } + + /// + /// + /// + public Exception OriginalException { get; internal set; } + + /// + /// + /// + public byte[] RequestBodyInBytes { get; internal set; } + + /// + /// + /// + public byte[] ResponseBodyInBytes { get; internal set; } + + /// + /// + /// + internal string ResponseMimeType { get; set; } + + /// + /// + /// + public bool Success { get; internal set; } + + /// + /// + /// + internal bool SuccessOrKnownError => + Success || HttpStatusCode >= 400 + && HttpStatusCode < 599 + && HttpStatusCode != 504 //Gateway timeout needs to be retried + && HttpStatusCode != 503 //service unavailable needs to be retried + && HttpStatusCode != 502; + + /// + /// + /// + public Uri Uri { get; internal set; } + + /// + /// + /// + internal ITransportConfiguration TransportConfiguration { get; set; } + + /// + /// + /// + internal IReadOnlyDictionary> ParsedHeaders { get; set; } + = EmptyReadOnly>.Dictionary; + + /// + /// + /// + /// + /// + /// + // TODO: Nullable annotations + public bool TryGetHeader(string key, out IEnumerable headerValues) => + ParsedHeaders.TryGetValue(key, out headerValues); + + /// + /// The error response if the server returned JSON describing a server error. + /// + internal ErrorResponse ErrorResponse { get; set; } = EmptyError.Instance; + + /// + /// A string summarising the API call. + /// + public override string ToString() => + $"{(Success ? "S" : "Uns")}uccessful ({HttpStatusCode}) low level call on {HttpMethod.GetStringValue()}: {(Uri is not null ? Uri.PathAndQuery: "UNKNOWN URI")}"; } diff --git a/src/Elastic.Transport/Responses/HttpDetails/IApiCallDetails.cs b/src/Elastic.Transport/Responses/HttpDetails/IApiCallDetails.cs deleted file mode 100644 index cd2b405..0000000 --- a/src/Elastic.Transport/Responses/HttpDetails/IApiCallDetails.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net.NetworkInformation; -using Elastic.Transport.Diagnostics; -using Elastic.Transport.Diagnostics.Auditing; - -namespace Elastic.Transport -{ - /// Details about the API call. - public interface IApiCallDetails - { - /// - /// An audit trail of requests made to nodes within the cluster. - /// - IEnumerable AuditTrail { get; } - - /// - /// Thread pool thread statistics collected when making a request. - /// - IReadOnlyDictionary ThreadPoolStats { get; } - - /// - /// Active TCP connection statistics collected when making a request. - /// - IReadOnlyDictionary TcpStats { get; } - - /// - /// A human readable string representation of what happened during this request for both successful and failed requests. - /// - string DebugInformation { get; } - - /// - /// Reference to the connection configuration that yielded this response. - /// - ITransportConfiguration TransportConfiguration { get; } - - /// - /// The HTTP method used by the request. - /// - HttpMethod HttpMethod { get; } - - /// The HTTP status code of the response. - int? HttpStatusCode { get; } - - /// - /// If is false, this will hold the original exception. - /// This will be the originating CLR exception in most cases. - /// - Exception OriginalException { get; } - - /// - /// A dictionary of the headers parsed from the HTTP response. - /// When no headers have been configured for parsing, or no matching headers were found, - /// this will be null. - /// - IReadOnlyDictionary> ParsedHeaders { get; } - - /// - /// The request body bytes. - /// NOTE: Only set when disable direct streaming is set for the request. - /// - [DebuggerDisplay("{RequestBodyInBytes != null ? System.Text.Encoding.UTF8.GetString(RequestBodyInBytes) : null,nq}")] - byte[] RequestBodyInBytes { get; } - - /// - /// The response body bytes. - /// NOTE: Only set when disable direct streaming is set for the request. - /// - [DebuggerDisplay("{ResponseBodyInBytes != null ? System.Text.Encoding.UTF8.GetString(ResponseBodyInBytes) : null,nq}")] - byte[] ResponseBodyInBytes { get; } - - /// The MIME type of the response. - string ResponseMimeType { get; } - - /// - /// The response status code is in the 200 range or is in the allowed list of status codes set on the request. - /// - bool Success { get; } - - /// - /// The response is successful or has a response code between 400-599, the call should not be retried. - /// Only for 502, 503 and 504 will this return false; - /// - bool SuccessOrKnownError { get; } - - /// - /// The from the request. - /// - Uri Uri { get; } - } -} diff --git a/src/Elastic.Transport/Responses/ITransportResponse.cs b/src/Elastic.Transport/Responses/ITransportResponse.cs deleted file mode 100644 index 8eec3d4..0000000 --- a/src/Elastic.Transport/Responses/ITransportResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.Transport -{ - /// - /// The minimum interface which custom responses should implement. - /// - public interface ITransportResponse - { - /// - /// sets the diagnostic information about the request and response. - /// - IApiCallDetails ApiCall { get; set; } - } -} diff --git a/src/Elastic.Transport/Responses/ResponseFactory.cs b/src/Elastic.Transport/Responses/ResponseFactory.cs new file mode 100644 index 0000000..20d6909 --- /dev/null +++ b/src/Elastic.Transport/Responses/ResponseFactory.cs @@ -0,0 +1,58 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Transport; + +/// +/// TODO +/// +public static class ResponseFactory +{ + /// + /// + /// + /// + /// + public static T CreateResponse(T response, int statusCode) where T : TransportResponse => + CreateResponse(response, statusCode, true); + + /// + /// + /// + /// + /// + /// + /// + /// + public static T CreateResponse(T response, int statusCode, bool success) where T : TransportResponse => + CreateResponse(response, statusCode, HttpMethod.GET, success); + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static T CreateResponse(T response, int statusCode, HttpMethod httpMethod, bool success) where T : TransportResponse + { + var apiCallDetails = new ApiCallDetails { HttpStatusCode = statusCode, Success = success, HttpMethod = httpMethod }; + return CreateResponse(response, apiCallDetails); + } + + /// + /// + /// + /// + /// + /// + /// + internal static T CreateResponse(T response, ApiCallDetails apiCallDetails) where T : TransportResponse + { + response.ApiCallDetails = apiCallDetails; + return response; + } +} diff --git a/src/Elastic.Transport/Responses/ResponseStatics.cs b/src/Elastic.Transport/Responses/ResponseStatics.cs index 8abbe95..51f199f 100644 --- a/src/Elastic.Transport/Responses/ResponseStatics.cs +++ b/src/Elastic.Transport/Responses/ResponseStatics.cs @@ -9,120 +9,118 @@ using Elastic.Transport.Diagnostics.Auditing; using Elastic.Transport.Extensions; -namespace Elastic.Transport +namespace Elastic.Transport.Diagnostics; + +/// +/// Creates human readable debug strings based on so that +/// its clear what exactly transpired during a call into +/// +internal static class ResponseStatics { - //TODO Put in diagnostics folder/namespace - /// - /// Creates human readable debug strings based on so that - /// its clear what exactly transpired during a call into - /// - internal static class ResponseStatics - { - private static readonly string RequestAlreadyCaptured = - ""; + private static readonly string RequestAlreadyCaptured = + ""; - private static readonly string ResponseAlreadyCaptured = - ""; + private static readonly string ResponseAlreadyCaptured = + ""; - /// - public static string DebugInformationBuilder(IApiCallDetails r, StringBuilder sb) + /// + public static string DebugInformationBuilder(ApiCallDetails r, StringBuilder sb) + { + sb.AppendLine($"# Audit trail of this API call:"); + var auditTrail = (r.AuditTrail ?? Enumerable.Empty()).ToList(); + DebugAuditTrail(auditTrail, sb); + if (r.OriginalException != null) sb.AppendLine($"# OriginalException: {r.OriginalException}"); + DebugAuditTrailExceptions(auditTrail, sb); + + var response = r.ResponseBodyInBytes?.Utf8String() ?? ResponseAlreadyCaptured; + var request = r.RequestBodyInBytes?.Utf8String() ?? RequestAlreadyCaptured; + sb.AppendLine($"# Request:{Environment.NewLine}{request}"); + sb.AppendLine($"# Response:{Environment.NewLine}{response}"); + + if (r.TcpStats != null) { - sb.AppendLine($"# Audit trail of this API call:"); - var auditTrail = (r.AuditTrail ?? Enumerable.Empty()).ToList(); - DebugAuditTrail(auditTrail, sb); - if (r.OriginalException != null) sb.AppendLine($"# OriginalException: {r.OriginalException}"); - DebugAuditTrailExceptions(auditTrail, sb); - - var response = r.ResponseBodyInBytes?.Utf8String() ?? ResponseAlreadyCaptured; - var request = r.RequestBodyInBytes?.Utf8String() ?? RequestAlreadyCaptured; - sb.AppendLine($"# Request:{Environment.NewLine}{request}"); - sb.AppendLine($"# Response:{Environment.NewLine}{response}"); - - if (r.TcpStats != null) + sb.AppendLine("# TCP states:"); + foreach (var stat in r.TcpStats) { - sb.AppendLine("# TCP states:"); - foreach (var stat in r.TcpStats) - { - sb.Append(" "); - sb.Append(stat.Key); - sb.Append(": "); - sb.AppendLine($"{stat.Value}"); - } - sb.AppendLine(); + sb.Append(" "); + sb.Append(stat.Key); + sb.Append(": "); + sb.AppendLine($"{stat.Value}"); } + sb.AppendLine(); + } - if (r.ThreadPoolStats != null) + if (r.ThreadPoolStats != null) + { + sb.AppendLine("# ThreadPool statistics:"); + foreach (var stat in r.ThreadPoolStats) { - sb.AppendLine("# ThreadPool statistics:"); - foreach (var stat in r.ThreadPoolStats) - { - sb.Append(" "); - sb.Append(stat.Key); - sb.AppendLine(": "); - sb.Append(" Busy: "); - sb.AppendLine($"{stat.Value.Busy}"); - sb.Append(" Free: "); - sb.AppendLine($"{stat.Value.Free}"); - sb.Append(" Min: "); - sb.AppendLine($"{stat.Value.Min}"); - sb.Append(" Max: "); - sb.AppendLine($"{stat.Value.Max}"); - } - sb.AppendLine(); + sb.Append(" "); + sb.Append(stat.Key); + sb.AppendLine(": "); + sb.Append(" Busy: "); + sb.AppendLine($"{stat.Value.Busy}"); + sb.Append(" Free: "); + sb.AppendLine($"{stat.Value.Free}"); + sb.Append(" Min: "); + sb.AppendLine($"{stat.Value.Min}"); + sb.Append(" Max: "); + sb.AppendLine($"{stat.Value.Max}"); } - - return sb.ToString(); + sb.AppendLine(); } - /// - /// Write the exceptions recorded in to in - /// a debuggable and human readable string - /// - public static void DebugAuditTrailExceptions(IEnumerable auditTrail, StringBuilder sb) - { - if (auditTrail == null) return; + return sb.ToString(); + } - var auditExceptions = auditTrail.Select((audit, i) => new { audit, i }).Where(a => a.audit.Exception != null); - foreach (var a in auditExceptions) - sb.AppendLine($"# Audit exception in step {a.i + 1} {a.audit.Event.GetStringValue()}:{Environment.NewLine}{a.audit.Exception}"); - } + /// + /// Write the exceptions recorded in to in + /// a debuggable and human readable string + /// + public static void DebugAuditTrailExceptions(IEnumerable auditTrail, StringBuilder sb) + { + if (auditTrail == null) return; - /// - /// Write the events recorded in to in - /// a debuggable and human readable string - /// - public static void DebugAuditTrail(IEnumerable auditTrail, StringBuilder sb) - { - if (auditTrail == null) return; + var auditExceptions = auditTrail.Select((audit, i) => new { audit, i }).Where(a => a.audit.Exception != null); + foreach (var a in auditExceptions) + sb.AppendLine($"# Audit exception in step {a.i + 1} {a.audit.Event.GetStringValue()}:{Environment.NewLine}{a.audit.Exception}"); + } - foreach (var a in auditTrail.Select((a, i) => new { a, i })) - { - var audit = a.a; - sb.Append($" - [{a.i + 1}] {audit.Event.GetStringValue()}:"); + /// + /// Write the events recorded in to in + /// a debuggable and human readable string + /// + public static void DebugAuditTrail(IEnumerable auditTrail, StringBuilder sb) + { + if (auditTrail == null) return; - AuditNodeUrl(sb, audit); + foreach (var a in auditTrail.Select((a, i) => new { a, i })) + { + var audit = a.a; + sb.Append($" - [{a.i + 1}] {audit.Event.GetStringValue()}:"); - if (audit.Exception != null) sb.Append($" Exception: {audit.Exception.GetType().Name}"); - if (audit.Ended == default) - sb.AppendLine(); - else sb.AppendLine($" Took: {(audit.Ended - audit.Started).ToString()}"); - } + AuditNodeUrl(sb, audit); + + if (audit.Exception != null) sb.Append($" Exception: {audit.Exception.GetType().Name}"); + if (audit.Ended == default) + sb.AppendLine(); + else sb.AppendLine($" Took: {(audit.Ended - audit.Started).ToString()}"); } + } - private static void AuditNodeUrl(StringBuilder sb, Audit audit) - { - var uri = audit.Node?.Uri; - if (uri == null) return; + private static void AuditNodeUrl(StringBuilder sb, Audit audit) + { + var uri = audit.Node?.Uri; + if (uri == null) return; - if (!string.IsNullOrEmpty(uri.UserInfo)) + if (!string.IsNullOrEmpty(uri.UserInfo)) + { + var builder = new UriBuilder(uri) { - var builder = new UriBuilder(uri) - { - Password = "redacted" - }; - uri = builder.Uri; - } - sb.Append($" Node: {uri}"); + Password = "redacted" + }; + uri = builder.Uri; } + sb.Append($" Node: {uri}"); } } diff --git a/src/Elastic.Transport/Responses/Special/BytesResponse.cs b/src/Elastic.Transport/Responses/Special/BytesResponse.cs index 77ec886..a4a3157 100644 --- a/src/Elastic.Transport/Responses/Special/BytesResponse.cs +++ b/src/Elastic.Transport/Responses/Special/BytesResponse.cs @@ -4,17 +4,16 @@ using System; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A response that exposes the response as byte array +/// +public sealed class BytesResponse : TransportResponse { - /// - /// A response that exposes the response as byte array - /// - public sealed class BytesResponse : TransportResponse - { - /// - public BytesResponse() => Body = Array.Empty(); + /// + public BytesResponse() => Body = Array.Empty(); - /// - public BytesResponse(byte[] body) => Body = body; - } + /// + public BytesResponse(byte[] body) => Body = body; } diff --git a/src/Elastic.Transport/Responses/Special/StringResponse.cs b/src/Elastic.Transport/Responses/Special/StringResponse.cs index b604b08..879fa5b 100644 --- a/src/Elastic.Transport/Responses/Special/StringResponse.cs +++ b/src/Elastic.Transport/Responses/Special/StringResponse.cs @@ -2,17 +2,16 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A response that exposes the response as . +/// +public sealed class StringResponse : TransportResponse { - /// - /// A response that exposes the response as . - /// - public sealed class StringResponse : TransportResponse - { - /// - public StringResponse() => Body = string.Empty; + /// + public StringResponse() => Body = string.Empty; - /// - public StringResponse(string body) => Body = body; - } + /// + public StringResponse(string body) => Body = body; } diff --git a/src/Elastic.Transport/Responses/Special/VoidResponse.cs b/src/Elastic.Transport/Responses/Special/VoidResponse.cs index 2dc1e70..58f4c9d 100644 --- a/src/Elastic.Transport/Responses/Special/VoidResponse.cs +++ b/src/Elastic.Transport/Responses/Special/VoidResponse.cs @@ -2,24 +2,23 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A special response that omits reading the response from the server after reading the headers. +/// +public sealed class VoidResponse : TransportResponse { + /// + public VoidResponse() => Body = new VoidBody(); + /// - /// A special response that omits reading the response from the server after reading the headers. + /// A static instance that can be reused. /// - public sealed class VoidResponse : TransportResponse - { - /// - public VoidResponse() => Body = new VoidBody(); - - /// - /// A static instance that can be reused. - /// - public static VoidResponse Default { get; } = new VoidResponse(); + public static VoidResponse Default { get; } = new VoidResponse(); - /// - /// A class that represents the absence of having read the servers response to completion. - /// - public class VoidBody { } - } + /// + /// A class that represents the absence of having read the servers response to completion. + /// + public class VoidBody { } } diff --git a/src/Elastic.Transport/Responses/TransportResponse.cs b/src/Elastic.Transport/Responses/TransportResponse.cs index 46f0aca..79c230b 100644 --- a/src/Elastic.Transport/Responses/TransportResponse.cs +++ b/src/Elastic.Transport/Responses/TransportResponse.cs @@ -2,94 +2,30 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System; -using System.Collections.Generic; -using System.Net.NetworkInformation; using System.Text.Json.Serialization; -using Elastic.Transport.Diagnostics; -using Elastic.Transport.Diagnostics.Auditing; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// A response from an Elastic product including details about the request/response life cycle. Base class for the built in low level response +/// types, , , and +/// +public abstract class TransportResponse : TransportResponse { /// - /// A response from an Elastic product including details about the request/response life cycle. Base class for the built in low level response - /// types, , , and + /// The deserialized body returned by the product. /// - public abstract class TransportResponse : TransportResponse - { - /// - /// The deserialized body returned by the product. - /// - public T Body { get; protected internal set; } - } + public T Body { get; protected internal set; } +} +/// +/// A response as returned by including details about the request/response life cycle. +/// +public abstract class TransportResponse +{ /// - /// A response as returned by including details about the request/response life cycle. + /// /// - public abstract class TransportResponse : IApiCallDetails, ITransportResponse - { - /// - [JsonIgnore] - public IApiCallDetails ApiCall { get; set; } - - /// - [JsonIgnore] - public IReadOnlyDictionary TcpStats => ApiCall.TcpStats; - - /// - [JsonIgnore] - public string DebugInformation => ApiCall.DebugInformation; - - /// - [JsonIgnore] - public HttpMethod HttpMethod => ApiCall.HttpMethod; - - /// - [JsonIgnore] - public IEnumerable AuditTrail => ApiCall.AuditTrail; - - /// - [JsonIgnore] - public IReadOnlyDictionary ThreadPoolStats => ApiCall.ThreadPoolStats; - - /// - [JsonIgnore] - public bool SuccessOrKnownError => ApiCall.SuccessOrKnownError; - /// - [JsonIgnore] - public int? HttpStatusCode => ApiCall.HttpStatusCode; - - /// - [JsonIgnore] - public bool Success => ApiCall.Success; - /// - [JsonIgnore] - public Exception OriginalException => ApiCall.OriginalException; - /// - [JsonIgnore] - public string ResponseMimeType => ApiCall.ResponseMimeType; - /// - [JsonIgnore] - public Uri Uri => ApiCall.Uri; - - /// - [JsonIgnore] - public ITransportConfiguration TransportConfiguration => ApiCall.TransportConfiguration; - - /// - [JsonIgnore] - public byte[] ResponseBodyInBytes => ApiCall.ResponseBodyInBytes; - - /// - [JsonIgnore] - public byte[] RequestBodyInBytes => ApiCall.RequestBodyInBytes; - - /// - [JsonIgnore] - public IReadOnlyDictionary> ParsedHeaders => ApiCall.ParsedHeaders; - - /// - public override string ToString() => ApiCall.ToString(); - } - + [JsonIgnore] + public ApiCallDetails ApiCallDetails { get; internal set; } } diff --git a/src/Elastic.Transport/StringExtensions.cs b/src/Elastic.Transport/StringExtensions.cs deleted file mode 100644 index 389e45d..0000000 --- a/src/Elastic.Transport/StringExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.Transport -{ - internal static class StringExtensions - { - internal static string ToCamelCase(this string s) - { - if (string.IsNullOrEmpty(s)) - return s; - - if (!char.IsUpper(s[0])) - return s; - - var camelCase = char.ToLowerInvariant(s[0]).ToString(); - if (s.Length > 1) - camelCase += s.Substring(1); - - return camelCase; - } - } -} diff --git a/src/Elastic.Transport/Transport.cs b/src/Elastic.Transport/Transport.cs deleted file mode 100644 index 4db5799..0000000 --- a/src/Elastic.Transport/Transport.cs +++ /dev/null @@ -1,404 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Elastic.Transport.Extensions; -using Elastic.Transport.Products; - -#if !DOTNETCORE -using System.Net; -#endif - -namespace Elastic.Transport -{ - /// - public class Transport : Transport - { - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different - /// nodes - /// - /// The connection settings to use for this transport - public Transport(TransportConfiguration configurationValues) : base(configurationValues) - { - } - - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different - /// nodes - /// - /// The connection settings to use for this transport - /// The date time proved to use, safe to pass null to use the default - /// The memory stream provider to use, safe to pass null to use the default - public Transport(TransportConfiguration configurationValues, - IDateTimeProvider dateTimeProvider = null, IMemoryStreamFactory memoryStreamFactory = null - ) - : base(configurationValues, null, dateTimeProvider, memoryStreamFactory) - { - } - - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different - /// nodes - /// - /// The connection settings to use for this transport - /// In charge of create a new pipeline, safe to pass null to use the default - /// The date time proved to use, safe to pass null to use the default - /// The memory stream provider to use, safe to pass null to use the default - internal Transport(TransportConfiguration configurationValues, - IRequestPipelineFactory pipelineProvider = null, - IDateTimeProvider dateTimeProvider = null, IMemoryStreamFactory memoryStreamFactory = null - ) - : base(configurationValues, pipelineProvider, dateTimeProvider, memoryStreamFactory) - { - } - } - - /// - public class Transport : ITransport - where TConfiguration : class, ITransportConfiguration - { - private readonly IProductRegistration _productRegistration; - - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different - /// nodes - /// - /// The connection settings to use for this transport - public Transport(TConfiguration configurationValues) : this(configurationValues, null, null, null) - { - } - - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different - /// nodes - /// - /// The connection settings to use for this transport - /// The date time proved to use, safe to pass null to use the default - /// The memory stream provider to use, safe to pass null to use the default - public Transport( - TConfiguration configurationValues, - IDateTimeProvider dateTimeProvider = null, - IMemoryStreamFactory memoryStreamFactory = null) - : this(configurationValues, null, dateTimeProvider, memoryStreamFactory) { } - - /// - /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on - /// different - /// nodes - /// - /// The connection settings to use for this transport - /// In charge of create a new pipeline, safe to pass null to use the default - /// The date time proved to use, safe to pass null to use the default - /// The memory stream provider to use, safe to pass null to use the default - public Transport( - TConfiguration configurationValues, - IRequestPipelineFactory pipelineProvider = null, - IDateTimeProvider dateTimeProvider = null, - IMemoryStreamFactory memoryStreamFactory = null - ) - { - configurationValues.ThrowIfNull(nameof(configurationValues)); - configurationValues.NodePool.ThrowIfNull(nameof(configurationValues.NodePool)); - configurationValues.Connection.ThrowIfNull(nameof(configurationValues.Connection)); - configurationValues.RequestResponseSerializer.ThrowIfNull(nameof(configurationValues - .RequestResponseSerializer)); - - _productRegistration = configurationValues.ProductRegistration; - Settings = configurationValues; - PipelineProvider = pipelineProvider ?? new RequestPipelineFactory(); - DateTimeProvider = dateTimeProvider ?? Elastic.Transport.DateTimeProvider.Default; - MemoryStreamFactory = memoryStreamFactory ?? configurationValues.MemoryStreamFactory; - } - - private IDateTimeProvider DateTimeProvider { get; } - private IMemoryStreamFactory MemoryStreamFactory { get; } - private IRequestPipelineFactory PipelineProvider { get; } - - /// - public TConfiguration Settings { get; } - - /// - public TResponse Request(HttpMethod method, string path, PostData data = null, - IRequestParameters requestParameters = null) - where TResponse : class, ITransportResponse, new() - { - using var pipeline = - PipelineProvider.Create(Settings, DateTimeProvider, MemoryStreamFactory, requestParameters); - - pipeline.FirstPoolUsage(Settings.BootstrapLock); - - var requestData = new RequestData(method, path, data, Settings, requestParameters, MemoryStreamFactory); - Settings.OnRequestDataCreated?.Invoke(requestData); - TResponse response = null; - - var seenExceptions = new List(); - - if (pipeline.TryGetSingleNode(out var singleNode)) - { - // No value in marking a single node as dead. We have no other options! - - requestData.Node = singleNode; - - try - { - response = pipeline.CallProductEndpoint(requestData); - } - catch (PipelineException pipelineException) when (!pipelineException.Recoverable) - { - HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); - } - catch (PipelineException pipelineException) - { - HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); - } - catch (Exception killerException) - { - ThrowUnexpectedTransportException(killerException, seenExceptions, requestData, response, pipeline); - } - } - else - foreach (var node in pipeline.NextNode()) - { - requestData.Node = node; - try - { - if (_productRegistration.SupportsSniff) pipeline.SniffOnStaleCluster(); - if (_productRegistration.SupportsPing) Ping(pipeline, node); - - response = pipeline.CallProductEndpoint(requestData); - if (!response.ApiCall.SuccessOrKnownError) - { - pipeline.MarkDead(node); - if (_productRegistration.SupportsSniff) pipeline.SniffOnConnectionFailure(); - } - } - catch (PipelineException pipelineException) when (!pipelineException.Recoverable) - { - HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); - break; - } - catch (PipelineException pipelineException) - { - HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); - } - catch (Exception killerException) - { - ThrowUnexpectedTransportException(killerException, seenExceptions, requestData, response, - pipeline); - } - - if (response == null || !response.ApiCall.SuccessOrKnownError) continue; // try the next node - - pipeline.MarkAlive(node); - break; - } - - return FinalizeResponse(requestData, pipeline, seenExceptions, response); - } - - /// - public async Task RequestAsync(HttpMethod method, string path, - PostData data = null, IRequestParameters requestParameters = null, - CancellationToken cancellationToken = default - ) - where TResponse : class, ITransportResponse, new() - { - using var pipeline = - PipelineProvider.Create(Settings, DateTimeProvider, MemoryStreamFactory, requestParameters); - - await pipeline.FirstPoolUsageAsync(Settings.BootstrapLock, cancellationToken).ConfigureAwait(false); - - var requestData = new RequestData(method, path, data, Settings, requestParameters, MemoryStreamFactory); - Settings.OnRequestDataCreated?.Invoke(requestData); - TResponse response = null; - - var seenExceptions = new List(); - - if (pipeline.TryGetSingleNode(out var singleNode)) - { - // No value in marking a single node as dead. We have no other options! - - requestData.Node = singleNode; - - try - { - response = await pipeline.CallProductEndpointAsync(requestData, cancellationToken) - .ConfigureAwait(false); - } - catch (PipelineException pipelineException) when (!pipelineException.Recoverable) - { - HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); - } - catch (PipelineException pipelineException) - { - HandlePipelineException(ref response, pipelineException, pipeline, singleNode, seenExceptions); - } - catch (Exception killerException) - { - ThrowUnexpectedTransportException(killerException, seenExceptions, requestData, response, pipeline); - } - } - else - foreach (var node in pipeline.NextNode()) - { - requestData.Node = node; - try - { - if (_productRegistration.SupportsSniff) - await pipeline.SniffOnStaleClusterAsync(cancellationToken).ConfigureAwait(false); - if (_productRegistration.SupportsPing) - await PingAsync(pipeline, node, cancellationToken).ConfigureAwait(false); - - response = await pipeline.CallProductEndpointAsync(requestData, cancellationToken) - .ConfigureAwait(false); - if (!response.ApiCall.SuccessOrKnownError) - { - pipeline.MarkDead(node); - if (_productRegistration.SupportsSniff) - await pipeline.SniffOnConnectionFailureAsync(cancellationToken).ConfigureAwait(false); - } - } - catch (PipelineException pipelineException) when (!pipelineException.Recoverable) - { - HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); - break; - } - catch (PipelineException pipelineException) - { - HandlePipelineException(ref response, pipelineException, pipeline, node, seenExceptions); - } - catch (Exception killerException) - { - if (killerException is OperationCanceledException && cancellationToken.IsCancellationRequested) - pipeline.AuditCancellationRequested(); - - throw new UnexpectedTransportException(killerException, seenExceptions) - { - Request = requestData, Response = response?.ApiCall, AuditTrail = pipeline.AuditTrail - }; - } - - if (cancellationToken.IsCancellationRequested) - { - pipeline.AuditCancellationRequested(); - break; - } - - if (response == null || !response.ApiCall.SuccessOrKnownError) continue; - - pipeline.MarkAlive(node); - break; - } - - return FinalizeResponse(requestData, pipeline, seenExceptions, response); - } - private static void ThrowUnexpectedTransportException(Exception killerException, - List seenExceptions, - RequestData requestData, - TResponse response, IRequestPipeline pipeline - ) where TResponse : class, ITransportResponse, new() => - throw new UnexpectedTransportException(killerException, seenExceptions) - { - Request = requestData, Response = response?.ApiCall, AuditTrail = pipeline.AuditTrail - }; - - private static void HandlePipelineException( - ref TResponse response, PipelineException ex, IRequestPipeline pipeline, Node node, - ICollection seenExceptions - ) - where TResponse : class, ITransportResponse, new() - { - response ??= ex.Response as TResponse; - pipeline.MarkDead(node); - seenExceptions.Add(ex); - } - - private TResponse FinalizeResponse(RequestData requestData, IRequestPipeline pipeline, - List seenExceptions, - TResponse response - ) where TResponse : class, ITransportResponse, new() - { - if (requestData.Node == null) //foreach never ran - pipeline.ThrowNoNodesAttempted(requestData, seenExceptions); - - var callDetails = GetMostRecentCallDetails(response, seenExceptions); - var clientException = pipeline.CreateClientException(response, callDetails, requestData, seenExceptions); - - if (response?.ApiCall == null) - pipeline.BadResponse(ref response, callDetails, requestData, clientException); - - HandleTransportException(requestData, clientException, response); - return response; - } - - private static IApiCallDetails GetMostRecentCallDetails(TResponse response, - IEnumerable seenExceptions) - where TResponse : class, ITransportResponse, new() - { - var callDetails = response?.ApiCall ?? seenExceptions.LastOrDefault(e => e.ApiCall != null)?.ApiCall; - return callDetails; - } - - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - private void HandleTransportException(RequestData data, Exception clientException, ITransportResponse response) - { - if (response.ApiCall is ApiCallDetails a) - { - //if original exception was not explicitly set during the pipeline - //set it to the TransportException we created for the bad response - if (clientException != null && a.OriginalException == null) - a.OriginalException = clientException; - //On .NET Core the ITransportClient implementation throws exceptions on bad responses - //This causes it to behave differently to .NET FULL. We already wrapped the WebException - //under TransportException and it exposes way more information as part of it's - //exception message e.g the the root cause of the server error body. -#if !DOTNETCORE - if (a.OriginalException is WebException) - a.OriginalException = clientException; -#endif - } - - Settings.OnRequestCompleted?.Invoke(response.ApiCall); - if (data != null && clientException != null && data.ThrowExceptions) throw clientException; - } - - private void Ping(IRequestPipeline pipeline, Node node) - { - try - { - pipeline.Ping(node); - } - catch (PipelineException e) when (e.Recoverable) - { - if (_productRegistration.SupportsSniff) - pipeline.SniffOnConnectionFailure(); - throw; - } - } - - private async Task PingAsync(IRequestPipeline pipeline, Node node, CancellationToken cancellationToken) - { - try - { - await pipeline.PingAsync(node, cancellationToken).ConfigureAwait(false); - } - catch (PipelineException e) when (e.Recoverable) - { - if (_productRegistration.SupportsSniff) - await pipeline.SniffOnConnectionFailureAsync(cancellationToken).ConfigureAwait(false); - throw; - } - } - } -} diff --git a/src/Elastic.Transport/TransportExtensions.cs b/src/Elastic.Transport/TransportExtensions.cs index 2b2677e..3c14e21 100644 --- a/src/Elastic.Transport/TransportExtensions.cs +++ b/src/Elastic.Transport/TransportExtensions.cs @@ -5,80 +5,79 @@ using System.Threading; using System.Threading.Tasks; -namespace Elastic.Transport +namespace Elastic.Transport; + +/// +/// Extends with some convenience methods to make it easier to perform specific requests +/// +public static class TransportExtensions { - /// - /// Extends with some convenience methods to make it easier to perform specific requests - /// - public static class TransportExtensions - { - /// Perform a GET request - public static TResponse Get(this ITransport transport, string path, - IRequestParameters parameters = null) - where TResponse : class, ITransportResponse, new() => - transport.Request(HttpMethod.GET, path, null, parameters); + /// Perform a GET request + public static TResponse Get(this HttpTransport transport, string path, + RequestParameters parameters = null) + where TResponse : TransportResponse, new() => + transport.Request(HttpMethod.GET, path, null, parameters); - /// Perform a GET request - public static Task GetAsync(this ITransport transport, string path, - IRequestParameters parameters = null, CancellationToken cancellationToken = default) - where TResponse : class, ITransportResponse, new() => - transport.RequestAsync(HttpMethod.GET, path, null, parameters, cancellationToken); + /// Perform a GET request + public static Task GetAsync(this HttpTransport transport, string path, + RequestParameters parameters = null, CancellationToken cancellationToken = default) + where TResponse : TransportResponse, new() => + transport.RequestAsync(HttpMethod.GET, path, null, parameters, cancellationToken); - /// Perform a HEAD request - public static TResponse Head(this ITransport transport, string path, - IRequestParameters parameters = null) - where TResponse : class, ITransportResponse, new() => - transport.Request(HttpMethod.HEAD, path, null, parameters); + /// Perform a HEAD request + public static TResponse Head(this HttpTransport transport, string path, + RequestParameters parameters = null) + where TResponse : TransportResponse, new() => + transport.Request(HttpMethod.HEAD, path, null, parameters); - /// Perform a HEAD request - public static Task HeadAsync(this ITransport transport, string path, - IRequestParameters parameters = null, CancellationToken cancellationToken = default) - where TResponse : class, ITransportResponse, new() => - transport.RequestAsync(HttpMethod.HEAD, path, null, parameters, cancellationToken); + /// Perform a HEAD request + public static Task HeadAsync(this HttpTransport transport, string path, + RequestParameters parameters = null, CancellationToken cancellationToken = default) + where TResponse : TransportResponse, new() => + transport.RequestAsync(HttpMethod.HEAD, path, null, parameters, cancellationToken); - /// Perform a HEAD request - public static VoidResponse Head(this ITransport transport, string path, IRequestParameters parameters = null) => - transport.Head(path, parameters); + /// Perform a HEAD request + public static VoidResponse Head(this HttpTransport transport, string path, RequestParameters parameters = null) => + transport.Head(path, parameters); - /// Perform a HEAD request - public static Task HeadAsync(this ITransport transport, string path, - IRequestParameters parameters = null, CancellationToken cancellationToken = default) => - transport.HeadAsync(path, parameters, cancellationToken); + /// Perform a HEAD request + public static Task HeadAsync(this HttpTransport transport, string path, + RequestParameters parameters = null, CancellationToken cancellationToken = default) => + transport.HeadAsync(path, parameters, cancellationToken); - /// Perform a POST request - public static TResponse Post(this ITransport transport, string path, PostData data, - IRequestParameters parameters = null) - where TResponse : class, ITransportResponse, new() => - transport.Request(HttpMethod.POST, path, data, parameters); + /// Perform a POST request + public static TResponse Post(this HttpTransport transport, string path, PostData data, + RequestParameters parameters = null) + where TResponse : TransportResponse, new() => + transport.Request(HttpMethod.POST, path, data, parameters); - /// Perform a POST request - public static Task PostAsync(this ITransport transport, string path, PostData data, - IRequestParameters parameters = null, CancellationToken cancellationToken = default) - where TResponse : class, ITransportResponse, new() => - transport.RequestAsync(HttpMethod.POST, path, data, parameters, cancellationToken); + /// Perform a POST request + public static Task PostAsync(this HttpTransport transport, string path, PostData data, + RequestParameters parameters = null, CancellationToken cancellationToken = default) + where TResponse : TransportResponse, new() => + transport.RequestAsync(HttpMethod.POST, path, data, parameters, cancellationToken); - /// Perform a PUT request - public static TResponse Put(this ITransport transport, string path, PostData data, - IRequestParameters parameters = null) - where TResponse : class, ITransportResponse, new() => - transport.Request(HttpMethod.PUT, path, data, parameters); + /// Perform a PUT request + public static TResponse Put(this HttpTransport transport, string path, PostData data, + RequestParameters parameters = null) + where TResponse : TransportResponse, new() => + transport.Request(HttpMethod.PUT, path, data, parameters); - /// Perform a PUT request - public static Task PutAsync(this ITransport transport, string path, PostData data, - IRequestParameters parameters = null, CancellationToken cancellationToken = default) - where TResponse : class, ITransportResponse, new() => - transport.RequestAsync(HttpMethod.PUT, path, data, parameters, cancellationToken); + /// Perform a PUT request + public static Task PutAsync(this HttpTransport transport, string path, PostData data, + RequestParameters parameters = null, CancellationToken cancellationToken = default) + where TResponse : TransportResponse, new() => + transport.RequestAsync(HttpMethod.PUT, path, data, parameters, cancellationToken); - /// Perform a DELETE request - public static TResponse Delete(this ITransport transport, string path, PostData data = null, - IRequestParameters parameters = null) - where TResponse : class, ITransportResponse, new() => - transport.Request(HttpMethod.DELETE, path, data, parameters); + /// Perform a DELETE request + public static TResponse Delete(this HttpTransport transport, string path, PostData data = null, + RequestParameters parameters = null) + where TResponse : TransportResponse, new() => + transport.Request(HttpMethod.DELETE, path, data, parameters); - /// Perform a DELETE request - public static Task DeleteAsync(this ITransport transport, string path, - PostData data = null, IRequestParameters parameters = null, CancellationToken cancellationToken = default) - where TResponse : class, ITransportResponse, new() => - transport.RequestAsync(HttpMethod.DELETE, path, data, parameters, cancellationToken); - } + /// Perform a DELETE request + public static Task DeleteAsync(this HttpTransport transport, string path, + PostData data = null, RequestParameters parameters = null, CancellationToken cancellationToken = default) + where TResponse : TransportResponse, new() => + transport.RequestAsync(HttpMethod.DELETE, path, data, parameters, cancellationToken); } diff --git a/tests/Elastic.Transport.IntegrationTests/Elastic.Transport.IntegrationTests.csproj b/tests/Elastic.Transport.IntegrationTests/Elastic.Transport.IntegrationTests.csproj index 1c17393..0e4e2b9 100644 --- a/tests/Elastic.Transport.IntegrationTests/Elastic.Transport.IntegrationTests.csproj +++ b/tests/Elastic.Transport.IntegrationTests/Elastic.Transport.IntegrationTests.csproj @@ -8,9 +8,7 @@ - - @@ -18,7 +16,6 @@ - diff --git a/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs b/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs index 12177d0..5e5699e 100644 --- a/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs +++ b/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs @@ -30,7 +30,7 @@ public TransferEncodingChunkedTests(TransportTestServer instance) : base(instanc private static readonly PostData Body = PostData.String(BodyString); private const string Path = "/chunked"; - private Transport Setup( + private HttpTransport Setup( TestableHttpConnection connection, Uri proxyAddress = null, bool? disableAutomaticProxyDetection = null, @@ -47,7 +47,7 @@ private Transport Setup( //make sure we the requests in debugging proxy : TransportTestServer.RerouteToProxyIfNeeded(config); - return new Transport(config); + return new DefaultHttpTransport(config); } /// diff --git a/tests/Elastic.Transport.IntegrationTests/Plumbing/AssemblyServerTestsBase.cs b/tests/Elastic.Transport.IntegrationTests/Plumbing/AssemblyServerTestsBase.cs index a18c929..3efa2a1 100644 --- a/tests/Elastic.Transport.IntegrationTests/Plumbing/AssemblyServerTestsBase.cs +++ b/tests/Elastic.Transport.IntegrationTests/Plumbing/AssemblyServerTestsBase.cs @@ -6,13 +6,13 @@ namespace Elastic.Transport.IntegrationTests.Plumbing { - public class AssemblyServerTestsBase : IAssemblyFixture where TServer : class, ITransportTestServer + public class AssemblyServerTestsBase : IAssemblyFixture where TServer : class, HttpTransportTestServer { public AssemblyServerTestsBase(TServer instance) => Server = instance; protected TServer Server { get; } - protected Transport Transport => Server.DefaultTransport; + protected HttpTransport Transport => Server.DefaultTransport; } public class AssemblyServerTestsBase : AssemblyServerTestsBase diff --git a/tests/Elastic.Transport.IntegrationTests/Plumbing/ClassServerTestsBase.cs b/tests/Elastic.Transport.IntegrationTests/Plumbing/ClassServerTestsBase.cs index 68291d8..7285160 100644 --- a/tests/Elastic.Transport.IntegrationTests/Plumbing/ClassServerTestsBase.cs +++ b/tests/Elastic.Transport.IntegrationTests/Plumbing/ClassServerTestsBase.cs @@ -6,12 +6,12 @@ namespace Elastic.Transport.IntegrationTests.Plumbing { - public class ClassServerTestsBase : IClassFixture where TServer : class, ITransportTestServer + public class ClassServerTestsBase : IClassFixture where TServer : class, HttpTransportTestServer { public ClassServerTestsBase(TServer instance) => Server = instance; protected TServer Server { get; } - protected Transport Transport => Server.DefaultTransport; + protected HttpTransport Transport => Server.DefaultTransport; } } diff --git a/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/ControllerIntegrationTests.cs b/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/ControllerIntegrationTests.cs index 8883319..903731e 100644 --- a/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/ControllerIntegrationTests.cs +++ b/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/ControllerIntegrationTests.cs @@ -27,7 +27,7 @@ public ControllerIntegrationTests(TransportTestServer instance) : base(instance) public async Task CanCallIntoController() { var response = await Transport.GetAsync("/dummy/20"); - response.Success.Should().BeTrue("{0}", response.DebugInformation); + response.ApiCallDetails.Success.Should().BeTrue("{0}", response.ApiCallDetails.DebugInformation); } } diff --git a/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/EndpointIntegrationTests.cs b/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/EndpointIntegrationTests.cs index 6aeb92c..693c8cb 100644 --- a/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/EndpointIntegrationTests.cs +++ b/tests/Elastic.Transport.IntegrationTests/Plumbing/Examples/EndpointIntegrationTests.cs @@ -25,7 +25,7 @@ public EndpointIntegrationTests(TransportTestServer instance) : ba public async Task CanCallIntoEndpoint() { var response = await Transport.GetAsync(DummyStartup.Endpoint); - response.Success.Should().BeTrue("{0}", response.DebugInformation); + response.ApiCallDetails.Success.Should().BeTrue("{0}", response.ApiCallDetails.DebugInformation); } } diff --git a/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs b/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs index a2f59dc..0b45362 100644 --- a/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs +++ b/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs @@ -17,11 +17,11 @@ namespace Elastic.Transport.IntegrationTests.Plumbing { - public interface ITransportTestServer + public interface HttpTransportTestServer { Uri Uri { get; } - Transport DefaultTransport { get; } + HttpTransport DefaultTransport { get; } } public class TransportTestServer : TransportTestServer @@ -39,12 +39,12 @@ public static TransportConfiguration RerouteToProxyIfNeeded(TransportConfigurati } - public class TransportTestServer : ITransportTestServer, IDisposable, IAsyncDisposable, IAsyncLifetime + public class TransportTestServer : HttpTransportTestServer, IDisposable, IAsyncDisposable, IAsyncLifetime where TStartup : class { private readonly IWebHost _host; private Uri _uri; - private Transport _defaultTransport; + private HttpTransport _defaultTransport; public TransportTestServer() { @@ -69,7 +69,7 @@ public Uri Uri private set => _uri = value; } - public Transport DefaultTransport + public HttpTransport DefaultTransport { get => _defaultTransport ?? throw new Exception($"{nameof(DefaultTransport)} is not available until {nameof(StartAsync)} is called"); private set => _defaultTransport = value; @@ -81,11 +81,11 @@ public async Task> StartAsync(CancellationToken to var port = _host.GetServerPort(); var url = $"http://{TransportTestServer.LocalOrProxyHost}:{port}"; Uri = new Uri(url); - DefaultTransport = CreateTransport(c => new Transport(c)); + DefaultTransport = CreateTransport(c => new DefaultHttpTransport(c)); return this; } - public Transport CreateTransport(Func create) => + public HttpTransport CreateTransport(Func create) => create(TransportTestServer.RerouteToProxyIfNeeded(new TransportConfiguration(Uri))); public void Dispose() => _host?.Dispose(); @@ -100,7 +100,5 @@ public ValueTask DisposeAsync() Dispose(); return ValueTask.CompletedTask; } - - } } diff --git a/tests/Elastic.Transport.Tests/CodeStandards/NamingConventions.doc.cs b/tests/Elastic.Transport.Tests/CodeStandards/NamingConventions.doc.cs index d40177d..ac1773f 100644 --- a/tests/Elastic.Transport.Tests/CodeStandards/NamingConventions.doc.cs +++ b/tests/Elastic.Transport.Tests/CodeStandards/NamingConventions.doc.cs @@ -24,7 +24,7 @@ [Fact] public void ClassNameContainsBaseShouldBeAbstract() { var exceptions = new Type[] { }; - var baseClassesNotAbstract = typeof(ITransport<>).Assembly.GetTypes() + var baseClassesNotAbstract = typeof(HttpTransport<>).Assembly.GetTypes() .Where(t => t.IsClass && !exceptions.Contains(t)) .Where(t => t.Name.Split('`')[0].EndsWith("Base")) .Where(t => !t.IsAbstract) @@ -36,7 +36,7 @@ [Fact] public void ClassNameContainsBaseShouldBeAbstract() private List Scan() { - var assembly = typeof(ITransport<>).Assembly; + var assembly = typeof(HttpTransport<>).Assembly; var exceptions = new List { diff --git a/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs b/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs index 21a7a03..b7a5cf5 100644 --- a/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs +++ b/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs @@ -119,25 +119,25 @@ protected override void Dispose(bool disposing) } } - private class TrackMemoryStreamFactory : IMemoryStreamFactory + private class TrackMemoryStreamFactory : MemoryStreamFactory { public IList Created { get; } = new List(); - public MemoryStream Create() + public override MemoryStream Create() { var stream = new TrackDisposeStream(); Created.Add(stream); return stream; } - public MemoryStream Create(byte[] bytes) + public override MemoryStream Create(byte[] bytes) { var stream = new TrackDisposeStream(bytes); Created.Add(stream); return stream; } - public MemoryStream Create(byte[] bytes, int index, int count) + public override MemoryStream Create(byte[] bytes, int index, int count) { var stream = new TrackDisposeStream(bytes, index, count); Created.Add(stream); diff --git a/tests/Elastic.Transport.Tests/Test.cs b/tests/Elastic.Transport.Tests/Test.cs index 887a1cb..2e0d3be 100644 --- a/tests/Elastic.Transport.Tests/Test.cs +++ b/tests/Elastic.Transport.Tests/Test.cs @@ -21,7 +21,7 @@ public void Usage() var product = ElasticsearchProductRegistration.Default; var settings = new TransportConfiguration(pool, connection, serializer, product); - var transport = new Transport(settings); + var transport = new DefaultHttpTransport(settings); var response = transport.Request(HttpMethod.GET, "/"); } @@ -29,7 +29,7 @@ public void Usage() public void MinimalUsage() { var settings = new TransportConfiguration(new Uri("http://localhost:9200")); - var transport = new Transport(settings); + var transport = new DefaultHttpTransport(settings); var response = transport.Get("/"); @@ -40,7 +40,7 @@ public void MinimalElasticsearch() { var uri = new Uri("http://localhost:9200"); var settings = new TransportConfiguration(uri, ElasticsearchProductRegistration.Default); - var transport = new Transport(settings); + var transport = new DefaultHttpTransport(settings); var response = transport.Get("/"); @@ -50,9 +50,9 @@ public void MinimalElasticsearch() public void MinimalUsageWithRequestParameters() { var settings = new TransportConfiguration(new Uri("http://localhost:9200")); - var transport = new Transport(settings); + var transport = new DefaultHttpTransport(settings); - var response = transport.Get("/", new RequestParameters()); + var response = transport.Get("/", new DefaultRequestParameters()); var headResponse = transport.Head("/"); } @@ -61,9 +61,9 @@ public class MyClientConfiguration : TransportConfigurationBase Assign(value, (c, v) => _setting = v); } - public class MyClientRequestPipeline : RequestPipeline + public class MyClientRequestPipeline : DefaultRequestPipeline { public MyClientRequestPipeline(MyClientConfiguration configurationValues, - IDateTimeProvider dateTimeProvider, IMemoryStreamFactory memoryStreamFactory, - IRequestParameters requestParameters) - : base(configurationValues, dateTimeProvider, memoryStreamFactory, requestParameters) + DateTimeProvider dateTimeProvider, MemoryStreamFactory memoryStreamFactory, + RequestParameters requestParameters) + : base(configurationValues, dateTimeProvider, memoryStreamFactory, requestParameters) { } } @@ -88,7 +88,8 @@ public void ExtendingConfiguration() { var clientConfiguration = new MyClientConfiguration() .NewSettings("some-value"); - var transport = new Transport(clientConfiguration); + + var transport = new DefaultHttpTransport(clientConfiguration); } } } diff --git a/tests/Elastic.Transport.Tests/VirtualClusterTests.cs b/tests/Elastic.Transport.Tests/VirtualClusterTests.cs index f7f9d23..80a2694 100644 --- a/tests/Elastic.Transport.Tests/VirtualClusterTests.cs +++ b/tests/Elastic.Transport.Tests/VirtualClusterTests.cs @@ -41,9 +41,9 @@ [Fact] public async Task ThrowsExceptionAfterDepleedingRules() { HealthyResponse, 9200, response => { - response.ApiCall.Success.Should().BeTrue(); - response.ApiCall.HttpStatusCode.Should().Be(200); - response.ApiCall.DebugInformation.Should().Contain("x\":1"); + response.ApiCallDetails.Success.Should().BeTrue(); + response.ApiCallDetails.HttpStatusCode.Should().Be(200); + response.ApiCallDetails.DebugInformation.Should().Contain("x\":1"); } }, } ); @@ -86,42 +86,42 @@ [Fact] public async Task RulesAreIgnoredAfterBeingExecuted() { HealthyResponse, 9200, response => { - response.ApiCall.Success.Should().BeTrue(); - response.ApiCall.HttpStatusCode.Should().Be(200); - response.ApiCall.DebugInformation.Should().Contain("x\":1"); + response.ApiCallDetails.Success.Should().BeTrue(); + response.ApiCallDetails.HttpStatusCode.Should().Be(200); + response.ApiCallDetails.DebugInformation.Should().Contain("x\":1"); } }, }, new ClientCall { { BadResponse, 9200, response => { - response.ApiCall.Success.Should().BeFalse(); - response.ApiCall.HttpStatusCode.Should().Be(500); - response.ApiCall.DebugInformation.Should().Contain("x\":2"); + response.ApiCallDetails.Success.Should().BeFalse(); + response.ApiCallDetails.HttpStatusCode.Should().Be(500); + response.ApiCallDetails.DebugInformation.Should().Contain("x\":2"); } }, }, new ClientCall { { BadResponse, 9200, response => { - response.ApiCall.HttpStatusCode.Should().Be(400); - response.ApiCall.DebugInformation.Should().Contain("x\":3"); + response.ApiCallDetails.HttpStatusCode.Should().Be(400); + response.ApiCallDetails.DebugInformation.Should().Contain("x\":3"); } }, }, new ClientCall { { BadResponse, 9200, response => { - response.ApiCall.HttpStatusCode.Should().Be(400); - response.ApiCall.DebugInformation.Should().Contain("x\":3"); + response.ApiCallDetails.HttpStatusCode.Should().Be(400); + response.ApiCallDetails.DebugInformation.Should().Contain("x\":3"); } }, }, new ClientCall { { HealthyResponse, 9200, response => { - response.ApiCall.HttpStatusCode.Should().Be(200); - response.ApiCall.DebugInformation.Should().Contain("x\":4"); + response.ApiCallDetails.HttpStatusCode.Should().Be(200); + response.ApiCallDetails.DebugInformation.Should().Contain("x\":4"); } }, } );