diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs b/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs index 4fd0586..b2601fe 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/ExposingPipelineFactory.cs @@ -8,25 +8,23 @@ 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 +public sealed class ExposingPipelineFactory : RequestPipelineFactory + where TConfiguration : class, ITransportConfiguration { public ExposingPipelineFactory(TConfiguration configuration, DateTimeProvider dateTimeProvider) { DateTimeProvider = dateTimeProvider; - MemoryStreamFactory = TransportConfiguration.DefaultMemoryStreamFactory; Configuration = configuration; - Pipeline = Create(Configuration, DateTimeProvider, MemoryStreamFactory, null); - RequestHandler = new DistributedTransport(Configuration, this, DateTimeProvider, MemoryStreamFactory); + Pipeline = Create(new RequestData(Configuration, null, null), DateTimeProvider); + RequestHandler = new DistributedTransport(Configuration, this, DateTimeProvider); } // ReSharper disable once MemberCanBePrivate.Global public RequestPipeline Pipeline { get; } private DateTimeProvider DateTimeProvider { get; } - private MemoryStreamFactory MemoryStreamFactory { get; } private TConfiguration Configuration { get; } public ITransport RequestHandler { get; } - public override RequestPipeline Create(TConfiguration configurationValues, DateTimeProvider dateTimeProvider, - MemoryStreamFactory memoryStreamFactory, IRequestConfiguration? requestConfiguration) => - new DefaultRequestPipeline(Configuration, DateTimeProvider, MemoryStreamFactory, requestConfiguration); + public override RequestPipeline Create(RequestData requestData, DateTimeProvider dateTimeProvider) => + new DefaultRequestPipeline(requestData, DateTimeProvider); } diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs b/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs index f479f92..f2b1c1b 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/SealedVirtualCluster.cs @@ -10,7 +10,7 @@ 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 +/// an instance of for the cluster after which the components such as /// and can no longer be updated. /// public sealed class SealedVirtualCluster @@ -28,23 +28,23 @@ internal SealedVirtualCluster(VirtualCluster cluster, NodePool pool, TestableDat _productRegistration = productRegistration; } - private TransportConfiguration CreateSettings() => + private TransportConfigurationDescriptor CreateSettings() => new(_nodePool, _requestInvoker, serializer: null, _productRegistration.ProductRegistration); - /// Create the cluster using all defaults on + /// 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) => + 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 VirtualClusterRequestInvoker VirtualClusterConnection(Func selector = null) => + public VirtualClusterRequestInvoker VirtualClusterConnection(Func selector = null) => new VirtualizedCluster(_dateTimeProvider, selector == null ? CreateSettings() : selector(CreateSettings())) .Connection; } diff --git a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs index 6ab83c4..20ddf3f 100644 --- a/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs +++ b/src/Elastic.Transport.VirtualizedCluster/Components/VirtualizedCluster.cs @@ -13,7 +13,7 @@ public class VirtualizedCluster { private readonly ExposingPipelineFactory _exposingRequestPipeline; private readonly TestableDateTimeProvider _dateTimeProvider; - private readonly TransportConfiguration _settings; + private readonly TransportConfigurationDescriptor _settings; private Func, Func, Task> _asyncCall; private Func, Func, TransportResponse> _syncCall; @@ -22,7 +22,7 @@ private class VirtualResponse : TransportResponse; private static readonly EndpointPath RootPath = new(HttpMethod.GET, "/"); - internal VirtualizedCluster(TestableDateTimeProvider dateTimeProvider, TransportConfiguration settings) + internal VirtualizedCluster(TestableDateTimeProvider dateTimeProvider, TransportConfigurationDescriptor settings) { _dateTimeProvider = dateTimeProvider; _settings = settings; diff --git a/src/Elastic.Transport/Components/Endpoint.cs b/src/Elastic.Transport/Components/Endpoint.cs new file mode 100644 index 0000000..699dc0e --- /dev/null +++ b/src/Elastic.Transport/Components/Endpoint.cs @@ -0,0 +1,66 @@ +// 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; + +/// +/// Represents the path of an endpoint in a transport request, including the HTTP method +/// and the path and query information. +/// +/// +/// This struct is used to store information about the HTTP method and the path and query of an endpoint, +/// which are essential components when constructing a request URI. +/// +public readonly record struct EndpointPath(HttpMethod Method, string PathAndQuery); + +/// +/// Represents an endpoint in a transport request, encapsulating the HTTP method, path and query, +/// and the node to which the request is being sent. +/// +/// +/// This class is used to construct the URI for the request based on the node's URI and the path and query. +/// An empty endpoint can be created using the method as a default or placeholder instance. +/// +public record Endpoint(in EndpointPath Path, Node Node) +{ + /// Represents an empty endpoint used as a default or placeholder instance of . + public static Endpoint Empty(in EndpointPath path) => new(path, EmptyNode); + + private static readonly Node EmptyNode = new(new Uri("http://empty.example")); + + /// Indicates whether the endpoint is an empty placeholder instance. + public bool IsEmpty => Node == EmptyNode; + + /// The for the request. + public Uri Uri { get; private init; } = new(Node.Uri, Path.PathAndQuery); + + /// The HTTP method used for the request (e.g., GET, POST, PUT, DELETE, HEAD). + public HttpMethod Method => Path.Method; + + /// Gets the path and query of the endpoint. + public string PathAndQuery => Path.PathAndQuery; + + private readonly Node _node = Node; + + /// + /// Represents a node within the transport layer of the Elastic search client. + /// This object encapsulates the characteristics of a node, allowing for comparisons and operations + /// within the broader search infrastructure. + /// + public Node Node + { + get => _node; + init + { + _node = value; + Uri = new(Node.Uri, Path.PathAndQuery); + } + } + + /// + public override string ToString() => $"{Path.Method.GetStringValue()} {Uri}"; + +} diff --git a/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs b/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs index af31cad..19b7d64 100644 --- a/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs +++ b/src/Elastic.Transport/Components/Pipeline/DefaultRequestPipeline.cs @@ -16,43 +16,40 @@ namespace Elastic.Transport; /// -public class DefaultRequestPipeline : RequestPipeline - where TConfiguration : class, ITransportConfiguration +public class DefaultRequestPipeline : RequestPipeline { private readonly IRequestInvoker _requestInvoker; private readonly NodePool _nodePool; + private readonly RequestData _requestData; 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; - private List _auditTrail = null; + private List? _auditTrail; + private readonly ITransportConfiguration _settings; /// - internal DefaultRequestPipeline( - TConfiguration configurationValues, - DateTimeProvider dateTimeProvider, - MemoryStreamFactory memoryStreamFactory, - IRequestConfiguration? requestConfiguration - ) + internal DefaultRequestPipeline(RequestData requestData, DateTimeProvider dateTimeProvider) { - _settings = configurationValues; - _nodePool = _settings.NodePool; - _requestInvoker = _settings.Connection; + _requestData = requestData; + _settings = requestData.ConnectionSettings; + + _nodePool = requestData.ConnectionSettings.NodePool; + _requestInvoker = requestData.ConnectionSettings.Connection; _dateTimeProvider = dateTimeProvider; - _memoryStreamFactory = memoryStreamFactory; - _productRegistration = configurationValues.ProductRegistration; + _memoryStreamFactory = requestData.MemoryStreamFactory; + _productRegistration = requestData.ConnectionSettings.ProductRegistration; _responseBuilder = _productRegistration.ResponseBuilder; - _nodePredicate = _settings.NodePredicate ?? _productRegistration.NodePredicate; - RequestConfig = requestConfiguration; + _nodePredicate = requestData.ConnectionSettings.NodePredicate ?? _productRegistration.NodePredicate; + StartedOn = dateTimeProvider.Now(); } /// - public override IEnumerable AuditTrail => _auditTrail ?? (IEnumerable)Array.Empty(); + public override IEnumerable AuditTrail => _auditTrail; private RequestConfiguration PingAndSniffRequestConfiguration { @@ -66,9 +63,9 @@ private RequestConfiguration PingAndSniffRequestConfiguration { PingTimeout = PingTimeout, RequestTimeout = PingTimeout, - Authentication = RequestConfig?.Authentication ?? _settings.Authentication, - EnableHttpPipelining = RequestConfig?.HttpPipeliningEnabled ?? _settings.HttpPipeliningEnabled, - ForceNode = RequestConfig?.ForceNode + Authentication = _requestData.AuthenticationHeader, + EnableHttpPipelining = _requestData.HttpPipeliningEnabled, + ForceNode = _requestData.ForceNode }; return _pingAndSniffRequestConfiguration; @@ -99,10 +96,7 @@ public override bool IsTakingTooLong } } - public override int MaxRetries => - RequestConfig?.ForceNode != null - ? 0 - : Math.Min(RequestConfig?.MaxRetries ?? _settings.MaxRetries.GetValueOrDefault(int.MaxValue), _nodePool.MaxRetries); + public override int MaxRetries => _requestData.MaxRetries; public bool Refresh { get; private set; } @@ -140,18 +134,13 @@ public override bool StaleClusterState public override DateTimeOffset StartedOn { get; } - private TimeSpan PingTimeout => - RequestConfig?.PingTimeout - ?? _settings.PingTimeout - ?? (_nodePool.UsingSsl ? RequestConfiguration.DefaultPingTimeoutOnSsl : RequestConfiguration.DefaultPingTimeout); + private TimeSpan PingTimeout => _requestData.PingTimeout; - private IRequestConfiguration RequestConfig { get; } + private bool RequestDisabledSniff => _requestData.DisableSniff; - private bool RequestDisabledSniff => RequestConfig != null && (RequestConfig.DisableSniff ?? false); + private TimeSpan RequestTimeout => _requestData.RequestTimeout; - private TimeSpan RequestTimeout => RequestConfig?.RequestTimeout ?? _settings.RequestTimeout ?? RequestConfiguration.DefaultRequestTimeout; - - public override void AuditCancellationRequested() => Audit(CancellationRequested).Dispose(); + public override void AuditCancellationRequested() => Audit(CancellationRequested)?.Dispose(); public override void BadResponse(ref TResponse response, ApiCallDetails callDetails, Endpoint endpoint, RequestData data, PostData? postData, TransportException exception) { @@ -362,9 +351,9 @@ public override bool TryGetSingleNode(out Node node) public override IEnumerable NextNode() { - if (RequestConfig?.ForceNode != null) + if (_requestData.ForceNode != null) { - yield return new Node(RequestConfig.ForceNode); + yield return new Node(_requestData.ForceNode); yield break; } @@ -416,15 +405,12 @@ public async ValueTask PingCoreAsync(bool isAsync, Node node, CancellationToken TransportResponse response; - //TODO remove - var requestData = new RequestData(_settings, null, null, _memoryStreamFactory); - try { if (isAsync) - response = await _productRegistration.PingAsync(_requestInvoker, pingEndpoint, requestData, cancellationToken).ConfigureAwait(false); + response = await _productRegistration.PingAsync(_requestInvoker, pingEndpoint, _requestData, cancellationToken).ConfigureAwait(false); else - response = _productRegistration.Ping(_requestInvoker, pingEndpoint, requestData); + response = _productRegistration.Ping(_requestInvoker, pingEndpoint, _requestData); ThrowBadAuthPipelineExceptionWhenNeeded(response.ApiCallDetails); @@ -462,7 +448,7 @@ public async ValueTask SniffCoreAsync(bool isAsync, CancellationToken cancellati { var sniffEndpoint = _productRegistration.CreateSniffEndpoint(node, PingAndSniffRequestConfiguration, _settings); //TODO remove - var requestData = new RequestData(_settings, null, null, _memoryStreamFactory); + var requestData = new RequestData(_settings, null, null); using var audit = Audit(SniffSuccess, node); @@ -554,9 +540,7 @@ public override void ThrowNoNodesAttempted(Endpoint endpoint, List - (RequestConfig?.DisablePings).GetValueOrDefault(false) - || (_settings.DisablePings ?? false) || !_nodePool.SupportsPinging || !node.IsResurrected; + private bool PingDisabled(Node node) => _requestData.DisablePings || !node.IsResurrected; private Auditable? Audit(AuditEvent type, Node node = null) => !_settings.DisableAuditTrail ?? true ? new(type, ref _auditTrail, _dateTimeProvider, node) : null; diff --git a/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs b/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs index 0bb90c3..88c8225 100644 --- a/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs +++ b/src/Elastic.Transport/Components/Pipeline/DefaultResponseBuilder.cs @@ -228,7 +228,7 @@ private async ValueTask SetBodyCoreAsync(bool isAsync, return response; } - if (!requestData.ValidateResponseContentType(mimeType)) + if (!ValidateResponseContentType(requestData.Accept, mimeType)) { ConditionalDisposal(responseStream, ownsStream, response); return default; diff --git a/src/Elastic.Transport/Components/Pipeline/PipelineException.cs b/src/Elastic.Transport/Components/Pipeline/PipelineException.cs index ba035a3..37f92e2 100644 --- a/src/Elastic.Transport/Components/Pipeline/PipelineException.cs +++ b/src/Elastic.Transport/Components/Pipeline/PipelineException.cs @@ -7,7 +7,7 @@ namespace Elastic.Transport; /// -/// A pipeline exception is throw when ever a known failing exit point is reached in +/// A pipeline exception is throw when ever a known failing exit point is reached in /// See for known exits points /// public class PipelineException : Exception diff --git a/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs b/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs index 585114b..4312188 100644 --- a/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs +++ b/src/Elastic.Transport/Components/Pipeline/PipelineFailure.cs @@ -7,7 +7,7 @@ namespace Elastic.Transport; /// -/// A failure in 's workflow that caused it to end prematurely. +/// A failure in 's workflow that caused it to end prematurely. /// public enum PipelineFailure { @@ -43,7 +43,7 @@ public enum PipelineFailure MaxRetriesReached, /// - /// An exception occurred during that could not be handled + /// An exception occurred during that could not be handled /// Unexpected, diff --git a/src/Elastic.Transport/Components/Pipeline/RequestData.cs b/src/Elastic.Transport/Components/Pipeline/RequestData.cs index 778ab7b..6cb1b50 100644 --- a/src/Elastic.Transport/Components/Pipeline/RequestData.cs +++ b/src/Elastic.Transport/Components/Pipeline/RequestData.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Elastic.Transport.Diagnostics; using Elastic.Transport.Diagnostics.Auditing; @@ -12,66 +13,6 @@ namespace Elastic.Transport; -/// -/// Represents the path of an endpoint in a transport request, including the HTTP method -/// and the path and query information. -/// -/// -/// This struct is used to store information about the HTTP method and the path and query of an endpoint, -/// which are essential components when constructing a request URI. -/// -public readonly record struct EndpointPath(HttpMethod Method, string PathAndQuery); - - -/// -/// Represents an endpoint in a transport request, encapsulating the HTTP method, path and query, -/// and the node to which the request is being sent. -/// -/// -/// This class is used to construct the URI for the request based on the node's URI and the path and query. -/// An empty endpoint can be created using the method as a default or placeholder instance. -/// -public record Endpoint(in EndpointPath Path, Node Node) -{ - /// Represents an empty endpoint used as a default or placeholder instance of . - public static Endpoint Empty(in EndpointPath path) => new(path, EmptyNode); - - private static readonly Node EmptyNode = new(new Uri("http://empty.example")); - - /// Indicates whether the endpoint is an empty placeholder instance. - public bool IsEmpty => Node == EmptyNode; - - /// The for the request. - public Uri Uri { get; private init; } = new(Node.Uri, Path.PathAndQuery); - - /// The HTTP method used for the request (e.g., GET, POST, PUT, DELETE, HEAD). - public HttpMethod Method => Path.Method; - - /// Gets the path and query of the endpoint. - public string PathAndQuery => Path.PathAndQuery; - - private readonly Node _node = Node; - - /// - /// Represents a node within the transport layer of the Elastic search client. - /// This object encapsulates the characteristics of a node, allowing for comparisons and operations - /// within the broader search infrastructure. - /// - public Node Node - { - get => _node; - init - { - _node = value; - Uri = new(Node.Uri, Path.PathAndQuery); - } - } - - /// - public override string ToString() => $"{Path.Method.GetStringValue()} {Uri}"; - -} - /// /// Where and how should connect to. /// @@ -79,26 +20,24 @@ public Node Node /// and . /// /// -public sealed class RequestData +public sealed record RequestData { -//TODO add xmldocs and clean up this class -#pragma warning disable 1591 + private const string OpaqueIdHeader = "X-Opaque-Id"; + + /// The default MIME type used for request and response payloads. public const string DefaultMimeType = "application/json"; - public const string OpaqueIdHeader = "X-Opaque-Id"; + + /// The security header used to run requests as a different user. public const string RunAsSecurityHeader = "es-security-runas-user"; - public RequestData( - ITransportConfiguration global, - IRequestConfiguration? local, - CustomResponseBuilder? customResponseBuilder, - MemoryStreamFactory memoryStreamFactory - ) + /// + public RequestData(ITransportConfiguration global, IRequestConfiguration? local = null, CustomResponseBuilder? customResponseBuilder = null) { CustomResponseBuilder = customResponseBuilder; ConnectionSettings = global; - MemoryStreamFactory = memoryStreamFactory; + MemoryStreamFactory = global.MemoryStreamFactory; - SkipDeserializationForStatusCodes = global.SkipDeserializationForStatusCodes; + SkipDeserializationForStatusCodes = global.SkipDeserializationForStatusCodes ?? []; DnsRefreshTimeout = global.DnsRefreshTimeout; MetaHeaderProvider = global.MetaHeaderProvider; ProxyAddress = global.ProxyAddress; @@ -113,7 +52,13 @@ MemoryStreamFactory memoryStreamFactory DisableDirectStreaming = local?.DisableDirectStreaming ?? global.DisableDirectStreaming ?? false; - Pipelined = local?.HttpPipeliningEnabled ?? global.HttpPipeliningEnabled ?? true; + ForceNode = global.ForceNode ?? local?.ForceNode; + MaxRetries = ForceNode != null ? 0 + : Math.Min(global.MaxRetries.GetValueOrDefault(int.MaxValue), global.NodePool.MaxRetries); + DisableSniff = global.DisableSniff ?? local?.DisableSniff ?? false; + DisablePings = global.DisablePings ?? !global.NodePool.SupportsPinging; + + HttpPipeliningEnabled = local?.HttpPipeliningEnabled ?? global.HttpPipeliningEnabled ?? true; HttpCompression = global.EnableHttpCompression ?? local?.EnableHttpCompression ?? true; ContentType = local?.ContentType ?? global.Accept ?? DefaultMimeType; Accept = local?.Accept ?? global.Accept ?? DefaultMimeType; @@ -124,8 +69,8 @@ MemoryStreamFactory memoryStreamFactory AllowedStatusCodes = local?.AllowedStatusCodes ?? EmptyReadOnly.Collection; ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates; TransferEncodingChunked = local?.TransferEncodingChunked ?? global.TransferEncodingChunked ?? false; - TcpStats = local?.EnableTcpStats ?? global.EnableTcpStats ?? true; - ThreadPoolStats = local?.EnableThreadPoolStats ?? global.EnableThreadPoolStats ?? true; + EnableTcpStats = local?.EnableTcpStats ?? global.EnableTcpStats ?? true; + EnableThreadPoolStats = local?.EnableThreadPoolStats ?? global.EnableThreadPoolStats ?? true; ParseAllHeaders = local?.ParseAllHeaders ?? global.ParseAllHeaders ?? false; ResponseHeadersToParse = local is not null ? new HeadersList(local.ResponseHeadersToParse, global.ResponseHeadersToParse) @@ -153,65 +98,81 @@ MemoryStreamFactory memoryStreamFactory } + /// + public MemoryStreamFactory MemoryStreamFactory { get; } + /// + public MetaHeaderProvider? MetaHeaderProvider { get; } + /// + public bool DisableAutomaticProxyDetection { get; } + /// + public int KeepAliveInterval { get; } + /// + public int KeepAliveTime { get; } + /// + public string? ProxyAddress { get; } + /// + public string? ProxyPassword { get; } + /// + public string? ProxyUsername { get; } + /// + public IReadOnlyCollection SkipDeserializationForStatusCodes { get; } + /// + public UserAgent UserAgent { get; } + /// + public TimeSpan DnsRefreshTimeout { get; } + + + /// + public IReadOnlyDictionary RequestMetaData { get; } + + /// 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 HeadersList? ResponseHeadersToParse { get; } - public NameValueCollection? Headers { get; } + /// + public NameValueCollection Headers { get; } + /// public bool DisableDirectStreaming { get; } + /// public bool ParseAllHeaders { get; } - public bool DisableAutomaticProxyDetection { get; } + /// public bool HttpCompression { get; } - public int KeepAliveInterval { get; } - public int KeepAliveTime { get; } - public MemoryStreamFactory MemoryStreamFactory { get; } - + /// + public Uri? ForceNode { get; } + /// public TimeSpan PingTimeout { get; } - public bool Pipelined { get; } - public string ProxyAddress { get; } - public string ProxyPassword { get; } - public string ProxyUsername { get; } + /// + public bool HttpPipeliningEnabled { get; } + /// public string ContentType { 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 TimeSpan DnsRefreshTimeout { get; } - - public MetaHeaderProvider? MetaHeaderProvider { get; } - - public IReadOnlyDictionary RequestMetaData { get; } - - //internal OpenTelemetryData OpenTelemetryData { get; } - - internal bool ValidateResponseContentType(string responseMimeType) - { - if (string.IsNullOrEmpty(responseMimeType)) return false; - - if (Accept == responseMimeType) - return true; - - // TODO - Performance: Review options to avoid the replace here and compare more efficiently. - var trimmedAccept = Accept.Replace(" ", ""); - var trimmedResponseMimeType = responseMimeType.Replace(" ", ""); - - return trimmedResponseMimeType.Equals(trimmedAccept, StringComparison.OrdinalIgnoreCase) - || trimmedResponseMimeType.StartsWith(trimmedAccept, StringComparison.OrdinalIgnoreCase) - - // ES specific fallback required because: - // - 404 responses from ES8 don't include the vendored header - // - ES8 EQL responses don't include vendored type - - || trimmedAccept.Contains("application/vnd.elasticsearch+json") && trimmedResponseMimeType.StartsWith(DefaultMimeType, StringComparison.OrdinalIgnoreCase); - } + /// + public bool EnableTcpStats { get; } + /// + public bool EnableThreadPoolStats { get; } + + /// + public int MaxRetries { get; } + /// + public bool DisableSniff { get; } + /// + public bool DisablePings { get; } -#pragma warning restore 1591 } diff --git a/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs b/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs index 79dc658..99343b3 100644 --- a/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs +++ b/src/Elastic.Transport/Components/Pipeline/RequestPipeline.cs @@ -15,7 +15,7 @@ namespace Elastic.Transport; /// public abstract class RequestPipeline : IDisposable { - private bool _disposed = false; + private bool _disposed; internal RequestPipeline() { } diff --git a/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs b/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs index 95f18c3..643089b 100644 --- a/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs +++ b/src/Elastic.Transport/Components/Pipeline/ResponseBuilder.cs @@ -83,7 +83,7 @@ long contentLength // We don't validate the content-type (MIME type) for HEAD requests or responses that have no content (204 status code). // Elastic Cloud responses to HEAD requests strip the content-type header so we want to avoid validation in that case. - var hasExpectedContentType = !MayHaveBody(statusCode, endpoint.Method, contentLength) || requestData.ValidateResponseContentType(mimeType); + var hasExpectedContentType = !MayHaveBody(statusCode, endpoint.Method, contentLength) || ValidateResponseContentType(requestData.Accept, mimeType); var details = new ApiCallDetails { @@ -114,4 +114,26 @@ long contentLength protected static bool MayHaveBody(int? statusCode, HttpMethod httpMethod, long contentLength) => contentLength != 0 && (!statusCode.HasValue || statusCode.Value != 204 && httpMethod != HttpMethod.HEAD); + internal static bool ValidateResponseContentType(string accept, string responseMimeType) + { + if (string.IsNullOrEmpty(responseMimeType)) return false; + + if (accept == responseMimeType) + return true; + + // TODO - Performance: Review options to avoid the replace here and compare more efficiently. + var trimmedAccept = accept.Replace(" ", ""); + var trimmedResponseMimeType = responseMimeType.Replace(" ", ""); + + return trimmedResponseMimeType.Equals(trimmedAccept, StringComparison.OrdinalIgnoreCase) + || trimmedResponseMimeType.StartsWith(trimmedAccept, StringComparison.OrdinalIgnoreCase) + + // ES specific fallback required because: + // - 404 responses from ES8 don't include the vendored header + // - ES8 EQL responses don't include vendored type + + || trimmedAccept.Contains("application/vnd.elasticsearch+json") + && trimmedResponseMimeType.StartsWith(RequestData.DefaultMimeType, StringComparison.OrdinalIgnoreCase); + } + } diff --git a/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs b/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs index 82779f2..0d137b3 100644 --- a/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs +++ b/src/Elastic.Transport/Components/Providers/DefaultRequestPipelineFactory.cs @@ -5,15 +5,13 @@ namespace Elastic.Transport; /// -/// The default implementation for that returns +/// The default implementation for that returns /// -internal sealed class DefaultRequestPipelineFactory : RequestPipelineFactory - where TConfiguration : class, ITransportConfiguration +internal sealed class DefaultRequestPipelineFactory : RequestPipelineFactory { /// - /// returns instances of + /// returns instances of /// - public override RequestPipeline Create(TConfiguration configurationValues, DateTimeProvider dateTimeProvider, - MemoryStreamFactory memoryStreamFactory, IRequestConfiguration? requestConfiguration) => - new DefaultRequestPipeline(configurationValues, dateTimeProvider, memoryStreamFactory, requestConfiguration); + public override RequestPipeline Create(RequestData requestData, DateTimeProvider dateTimeProvider) => + new DefaultRequestPipeline(requestData, dateTimeProvider); } diff --git a/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs b/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs index 25c8823..ff4ef38 100644 --- a/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs +++ b/src/Elastic.Transport/Components/Providers/RequestPipelineFactory.cs @@ -5,12 +5,10 @@ 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 +public abstract class RequestPipelineFactory { internal RequestPipelineFactory() { } /// Create an instance of - public abstract RequestPipeline Create(TConfiguration configuration, DateTimeProvider dateTimeProvider, - MemoryStreamFactory memoryStreamFactory, IRequestConfiguration? requestParameters); + public abstract RequestPipeline Create(RequestData requestData, DateTimeProvider dateTimeProvider); } diff --git a/src/Elastic.Transport/Components/TransportClient/HttpRequestInvoker.cs b/src/Elastic.Transport/Components/TransportClient/HttpRequestInvoker.cs index 00031d8..5593e1a 100644 --- a/src/Elastic.Transport/Components/TransportClient/HttpRequestInvoker.cs +++ b/src/Elastic.Transport/Components/TransportClient/HttpRequestInvoker.cs @@ -26,8 +26,8 @@ namespace Elastic.Transport; public class HttpRequestInvoker : IRequestInvoker { private static readonly string MissingConnectionLimitMethodError = - $"Your target platform does not support {nameof(TransportConfiguration.ConnectionLimit)}" - + $" please set {nameof(TransportConfiguration.ConnectionLimit)} to -1 on your configuration." + $"Your target platform does not support {nameof(TransportConfigurationDescriptor.ConnectionLimit)}" + + $" please set {nameof(TransportConfigurationDescriptor.ConnectionLimit)} to -1 on your configuration." + $" this will cause the {nameof(HttpClientHandler.MaxConnectionsPerServer)} not to be set on {nameof(HttpClientHandler)}"; private string _expectedCertificateFingerprint; @@ -96,10 +96,10 @@ private async ValueTask RequestCoreAsync(bool isAsync, End using (requestMessage?.Content ?? (IDisposable)Stream.Null) { - if (requestData.TcpStats) + if (requestData.EnableTcpStats) tcpStats = TcpStats.GetStates(); - if (requestData.ThreadPoolStats) + if (requestData.EnableThreadPoolStats) threadPoolStats = ThreadPoolStats.GetStats(); var prepareRequestMs = (Stopwatch.GetTimestamp() - beforeTicks) / (Stopwatch.Frequency / 1000); diff --git a/src/Elastic.Transport/Components/TransportClient/HttpWebRequestInvoker.cs b/src/Elastic.Transport/Components/TransportClient/HttpWebRequestInvoker.cs index d64c0cf..6a10c5c 100644 --- a/src/Elastic.Transport/Components/TransportClient/HttpWebRequestInvoker.cs +++ b/src/Elastic.Transport/Components/TransportClient/HttpWebRequestInvoker.cs @@ -125,10 +125,10 @@ private async ValueTask RequestCoreAsync(bool isAsync, End //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 - if (requestData.TcpStats) + if (requestData.EnableTcpStats) tcpStats = TcpStats.GetStates(); - if (requestData.ThreadPoolStats) + if (requestData.EnableThreadPoolStats) threadPoolStats = ThreadPoolStats.GetStats(); HttpWebResponse httpWebResponse; @@ -332,7 +332,7 @@ private static HttpWebRequest CreateWebRequest(Endpoint endpoint, RequestData re // on netstandard/netcoreapp2.0 this throws argument exception request.MaximumResponseHeadersLength = -1; #endif - request.Pipelined = requestData.Pipelined; + request.Pipelined = requestData.HttpPipeliningEnabled; if (requestData.TransferEncodingChunked) request.SendChunked = true; diff --git a/src/Elastic.Transport/Configuration/ITransportConfiguration.cs b/src/Elastic.Transport/Configuration/ITransportConfiguration.cs index f8abe90..0cd334b 100644 --- a/src/Elastic.Transport/Configuration/ITransportConfiguration.cs +++ b/src/Elastic.Transport/Configuration/ITransportConfiguration.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Net.Security; -using System.Security; using System.Security.Cryptography.X509Certificates; using System.Threading; using Elastic.Transport.Products; @@ -91,7 +90,7 @@ public interface ITransportConfiguration : IRequestConfiguration, IDisposable Func? NodePredicate { get; } /// - /// Allows you to register a callback every time a an API call is returned + /// Allows you to register a callback every time an API call is returned /// Action? OnRequestCompleted { get; } @@ -104,17 +103,17 @@ public interface ITransportConfiguration : IRequestConfiguration, IDisposable /// /// When set will force all connections through this proxy /// - string ProxyAddress { get; } + string? ProxyAddress { get; } /// /// The password for the proxy, when configured /// - string ProxyPassword { get; } + string? ProxyPassword { get; } /// /// The username for the proxy, when configured /// - string ProxyUsername { get; } + string? ProxyUsername { get; } /// /// Append these query string parameters automatically to every request @@ -127,19 +126,19 @@ public interface ITransportConfiguration : IRequestConfiguration, IDisposable /// /// Register a ServerCertificateValidationCallback per request /// - Func ServerCertificateValidationCallback { get; } + 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; } + 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; } + IReadOnlyCollection? SkipDeserializationForStatusCodes { get; } /// /// Force a new sniff for the cluster when the cluster state information is older than @@ -171,7 +170,7 @@ public interface ITransportConfiguration : IRequestConfiguration, IDisposable /// /// 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 + /// Defaults to validating the statusCode is greater or equal to 200 and less than 300 /// /// /// When the request is using 404 is valid out of the box as well diff --git a/src/Elastic.Transport/Configuration/RequestConfiguration.cs b/src/Elastic.Transport/Configuration/RequestConfiguration.cs index c0b9ba6..8a2c697 100644 --- a/src/Elastic.Transport/Configuration/RequestConfiguration.cs +++ b/src/Elastic.Transport/Configuration/RequestConfiguration.cs @@ -18,68 +18,68 @@ public interface IRequestConfiguration /// /// Force a different Accept header on the request /// - string? Accept { get; set; } + string? Accept { get; } /// /// Treat the following statuses (on top of the 200 range) NOT as error. /// - IReadOnlyCollection? AllowedStatusCodes { get; set; } + IReadOnlyCollection? AllowedStatusCodes { get; } /// Provide an authentication header override for this request - AuthorizationHeader? Authentication { get; set; } + AuthorizationHeader? Authentication { get; } /// /// Use the following client certificates to authenticate this single request /// - X509CertificateCollection? ClientCertificates { get; set; } + X509CertificateCollection? ClientCertificates { get; } /// /// Force a different Content-Type header on the request /// - string? ContentType { get; set; } + string? ContentType { get; } /// /// Whether to buffer the request and response bytes for the call /// - bool? DisableDirectStreaming { get; set; } + bool? DisableDirectStreaming { get; } /// /// Whether to disable the audit trail for the request. /// - bool? DisableAuditTrail { get; set; } + bool? DisableAuditTrail { get; } /// /// 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? DisablePings { get; set; } + bool? DisablePings { get; } /// /// Forces no sniffing to occur on the request no matter what configuration is in place /// globally /// - bool? DisableSniff { get; set; } + bool? DisableSniff { get; } /// /// Whether or not this request should be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining defaults to true /// - bool? HttpPipeliningEnabled { get; set; } + bool? HttpPipeliningEnabled { get; } /// /// Enable gzip compressed requests and responses /// - bool? EnableHttpCompression { get; set; } + bool? EnableHttpCompression { get; } /// /// This will force the operation on the specified node, this will bypass any configured connection pool and will no retry. /// - Uri? ForceNode { get; set; } + Uri? ForceNode { 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; set; } + int? MaxRetries { get; } /// /// Limits the total runtime including retries separately from @@ -87,74 +87,73 @@ public interface IRequestConfiguration /// When not specified defaults to which itself defaults to 60 seconds /// /// - TimeSpan? MaxRetryTimeout { get; set; } + TimeSpan? MaxRetryTimeout { get; } /// /// 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; } + string? OpaqueId { get; } /// Determines whether to parse all HTTP headers in the request. - bool? ParseAllHeaders { get; set; } + bool? ParseAllHeaders { get; } /// /// The ping timeout for this specific request /// - TimeSpan? PingTimeout { get; set; } + TimeSpan? PingTimeout { get; } /// - /// The timeout for this specific request, takes precedence over the global timeout settings + /// The timeout for this specific request, takes precedence over the global timeout init /// - TimeSpan? RequestTimeout { get; set; } + TimeSpan? RequestTimeout { get; } /// Specifies the headers from the response that should be parsed. - HeadersList? ResponseHeadersToParse { get; set; } + HeadersList? ResponseHeadersToParse { get; } /// /// 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; } + string? RunAs { 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; set; } + bool? ThrowExceptions { get; } /// /// Whether the request should be sent with chunked Transfer-Encoding. /// - bool? TransferEncodingChunked { get; set; } + bool? TransferEncodingChunked { get; } /// /// Try to send these headers for this single request /// - NameValueCollection? Headers { get; set; } + NameValueCollection? Headers { get; } /// /// Enable statistics about TCP connections to be collected when making a request /// - bool? EnableTcpStats { get; set; } + bool? EnableTcpStats { get; } /// /// Enable statistics about thread pools to be collected when making a request /// - bool? EnableThreadPoolStats { get; set; } + bool? EnableThreadPoolStats { get; } /// /// Holds additional meta data about the request. /// - RequestMetaData? RequestMetaData { get; set; } + RequestMetaData? RequestMetaData { get; } } /// -public class RequestConfiguration : IRequestConfiguration +public record RequestConfiguration : IRequestConfiguration { - /// The default request timeout. Defaults to 1 minute public static readonly TimeSpan DefaultRequestTimeout = TimeSpan.FromMinutes(10); @@ -166,88 +165,88 @@ public class RequestConfiguration : IRequestConfiguration /// - public string? Accept { get; set; } + public string? Accept { get; init; } /// - public IReadOnlyCollection? AllowedStatusCodes { get; set; } + public IReadOnlyCollection? AllowedStatusCodes { get; init; } /// - public AuthorizationHeader? Authentication { get; set; } + public AuthorizationHeader? Authentication { get; init; } /// - public X509CertificateCollection? ClientCertificates { get; set; } + public X509CertificateCollection? ClientCertificates { get; init; } /// - public string ContentType { get; set; } + public string? ContentType { get; init; } /// - public bool? DisableDirectStreaming { get; set; } + public bool? DisableDirectStreaming { get; init; } /// - public bool? DisableAuditTrail { get; set; } + public bool? DisableAuditTrail { get; init; } /// - public bool? DisablePings { get; set; } + public bool? DisablePings { get; init; } /// - public bool? DisablePing { get; set; } + public bool? DisablePing { get; init; } /// - public bool? DisableSniff { get; set; } + public bool? DisableSniff { get; init; } /// - public bool? HttpPipeliningEnabled { get; set; } + public bool? HttpPipeliningEnabled { get; init; } /// - public bool? EnableHttpPipelining { get; set; } = true; + public bool? EnableHttpPipelining { get; init; } = true; /// - public bool? EnableHttpCompression { get; set; } + public bool? EnableHttpCompression { get; init; } /// - public Uri ForceNode { get; set; } + public Uri? ForceNode { get; init; } /// - public int? MaxRetries { get; set; } + public int? MaxRetries { get; init; } /// - public TimeSpan? MaxRetryTimeout { get; set; } + public TimeSpan? MaxRetryTimeout { get; init; } /// - public string OpaqueId { get; set; } + public string? OpaqueId { get; init; } /// - public TimeSpan? PingTimeout { get; set; } + public TimeSpan? PingTimeout { get; init; } /// - public TimeSpan? RequestTimeout { get; set; } + public TimeSpan? RequestTimeout { get; init; } /// - public string RunAs { get; set; } + public string? RunAs { get; init; } /// - public bool? ThrowExceptions { get; set; } + public bool? ThrowExceptions { get; init; } /// - public bool? TransferEncodingChunked { get; set; } + public bool? TransferEncodingChunked { get; init; } /// - public NameValueCollection Headers { get; set; } + public NameValueCollection? Headers { get; init; } /// - public bool? EnableTcpStats { get; set; } + public bool? EnableTcpStats { get; init; } /// - public bool? EnableThreadPoolStats { get; set; } + public bool? EnableThreadPoolStats { get; init; } /// - public HeadersList? ResponseHeadersToParse { get; set; } + public HeadersList? ResponseHeadersToParse { get; init; } /// - public bool? ParseAllHeaders { get; set; } + public bool? ParseAllHeaders { get; init; } /// - public RequestMetaData RequestMetaData { get; set; } + public RequestMetaData? RequestMetaData { get; init; } } @@ -255,162 +254,162 @@ public class RequestConfiguration : IRequestConfiguration public class RequestConfigurationDescriptor : IRequestConfiguration { /// - public RequestConfigurationDescriptor() - { - } - - string IRequestConfiguration.Accept { get; set; } - IReadOnlyCollection IRequestConfiguration.AllowedStatusCodes { get; set; } - AuthorizationHeader IRequestConfiguration.Authentication { get; set; } - X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; } - string IRequestConfiguration.ContentType { get; set; } - bool? IRequestConfiguration.DisableDirectStreaming { get; set; } - bool? IRequestConfiguration.DisableAuditTrail { get; set; } - bool? IRequestConfiguration.DisablePings { get; set; } - bool? IRequestConfiguration.DisableSniff { get; set; } - bool? IRequestConfiguration.HttpPipeliningEnabled { get; set; } - bool? IRequestConfiguration.EnableHttpCompression { get; set; } - Uri IRequestConfiguration.ForceNode { get; set; } - int? IRequestConfiguration.MaxRetries { get; set; } - TimeSpan? IRequestConfiguration.MaxRetryTimeout { 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() { } + + private string? _accept; + private IReadOnlyCollection? _allowedStatusCodes; + private AuthorizationHeader? _authentication; + private X509CertificateCollection? _clientCertificates; + private string? _contentType; + private bool? _disableDirectStreaming; + private bool? _disableAuditTrail; + private bool? _disablePings; + private bool? _disableSniff; + private bool? _httpPipeliningEnabled; + private Uri? _forceNode; + private int? _maxRetries; + private string? _opaqueId; + private bool? _parseAllHeaders; + private TimeSpan? _pingTimeout; + private TimeSpan? _requestTimeout; + private HeadersList? _responseHeadersToParse; + private string? _runAs; + private bool? _throwExceptions; + private bool? _transferEncodingChunked; + private NameValueCollection? _headers; + private bool? _enableTcpStats; + private bool? _enableThreadPoolStats; + private RequestMetaData? _requestMetaData; + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private bool? _enableHttpCompression; + private TimeSpan? _maxRetryTimeout; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value /// public RequestConfigurationDescriptor RunAs(string username) { - Self.RunAs = username; + _runAs = username; return this; } /// public RequestConfigurationDescriptor RequestTimeout(TimeSpan requestTimeout) { - Self.RequestTimeout = requestTimeout; + _requestTimeout = requestTimeout; return this; } /// public RequestConfigurationDescriptor OpaqueId(string opaqueId) { - Self.OpaqueId = opaqueId; + _opaqueId = opaqueId; return this; } /// public RequestConfigurationDescriptor PingTimeout(TimeSpan pingTimeout) { - Self.PingTimeout = pingTimeout; + _pingTimeout = pingTimeout; return this; } /// public RequestConfigurationDescriptor ContentType(string contentTypeHeader) { - Self.ContentType = contentTypeHeader; + _contentType = contentTypeHeader; return this; } /// public RequestConfigurationDescriptor Accept(string acceptHeader) { - Self.Accept = acceptHeader; + _accept = acceptHeader; return this; } /// - public RequestConfigurationDescriptor AllowedStatusCodes(IEnumerable codes) + public RequestConfigurationDescriptor AllowedStatusCodes(IEnumerable? codes) { - Self.AllowedStatusCodes = codes?.ToReadOnlyCollection(); + _allowedStatusCodes = codes?.ToReadOnlyCollection(); return this; } /// public RequestConfigurationDescriptor AllowedStatusCodes(params int[] codes) { - Self.AllowedStatusCodes = codes?.ToReadOnlyCollection(); + _allowedStatusCodes = codes.ToReadOnlyCollection(); return this; } /// public RequestConfigurationDescriptor DisableSniffing(bool disable = true) { - Self.DisableSniff = disable; + _disableSniff = disable; return this; } /// public RequestConfigurationDescriptor DisablePing(bool disable = true) { - Self.DisablePings = disable; + _disablePings = disable; return this; } /// public RequestConfigurationDescriptor ThrowExceptions(bool throwExceptions = true) { - Self.ThrowExceptions = throwExceptions; + _throwExceptions = throwExceptions; return this; } /// public RequestConfigurationDescriptor DisableDirectStreaming(bool disable = true) { - Self.DisableDirectStreaming = disable; + _disableDirectStreaming = disable; return this; } /// public RequestConfigurationDescriptor DisableAuditTrail(bool disable = true) { - Self.DisableAuditTrail = disable; + _disableAuditTrail = disable; return this; } /// public RequestConfigurationDescriptor ForceNode(Uri uri) { - Self.ForceNode = uri; + _forceNode = uri; return this; } /// public RequestConfigurationDescriptor MaxRetries(int retry) { - Self.MaxRetries = retry; + _maxRetries = retry; return this; } /// public RequestConfigurationDescriptor Authentication(AuthorizationHeader authentication) { - Self.Authentication = authentication; + _authentication = authentication; return this; } /// public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true) { - Self.HttpPipeliningEnabled = enable; + _httpPipeliningEnabled = enable; return this; } /// public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates) { - Self.ClientCertificates = certificates; + _clientCertificates = certificates; return this; } @@ -425,49 +424,101 @@ public RequestConfigurationDescriptor ClientCertificate(string certificatePath) /// public RequestConfigurationDescriptor TransferEncodingChunked(bool transferEncodingChunked = true) { - Self.TransferEncodingChunked = transferEncodingChunked; + _transferEncodingChunked = transferEncodingChunked; return this; } /// public RequestConfigurationDescriptor GlobalHeaders(NameValueCollection headers) { - Self.Headers = headers; + _headers = headers; return this; } /// public RequestConfigurationDescriptor EnableTcpStats(bool enableTcpStats = true) { - Self.EnableTcpStats = enableTcpStats; + _enableTcpStats = enableTcpStats; return this; } /// public RequestConfigurationDescriptor EnableThreadPoolStats(bool enableThreadPoolStats = true) { - Self.EnableThreadPoolStats = enableThreadPoolStats; + _enableThreadPoolStats = enableThreadPoolStats; return this; } /// public RequestConfigurationDescriptor ParseAllHeaders(bool enable = true) { - Self.ParseAllHeaders = enable; + _parseAllHeaders = enable; return this; } /// public RequestConfigurationDescriptor ResponseHeadersToParse(IEnumerable headers) { - Self.ResponseHeadersToParse = new HeadersList(headers); + _responseHeadersToParse = new HeadersList(headers); return this; } /// public RequestConfigurationDescriptor RequestMetaData(RequestMetaData metaData) { - Self.RequestMetaData = metaData; + _requestMetaData = metaData; return this; } + + string? IRequestConfiguration.Accept => _accept; + + IReadOnlyCollection? IRequestConfiguration.AllowedStatusCodes => _allowedStatusCodes; + + AuthorizationHeader? IRequestConfiguration.Authentication => _authentication; + + X509CertificateCollection? IRequestConfiguration.ClientCertificates => _clientCertificates; + + string? IRequestConfiguration.ContentType => _contentType; + + bool? IRequestConfiguration.DisableDirectStreaming => _disableDirectStreaming; + + bool? IRequestConfiguration.DisableAuditTrail => _disableAuditTrail; + + bool? IRequestConfiguration.DisablePings => _disablePings; + + bool? IRequestConfiguration.DisableSniff => _disableSniff; + + bool? IRequestConfiguration.HttpPipeliningEnabled => _httpPipeliningEnabled; + + bool? IRequestConfiguration.EnableHttpCompression => _enableHttpCompression; + + Uri? IRequestConfiguration.ForceNode => _forceNode; + + int? IRequestConfiguration.MaxRetries => _maxRetries; + + TimeSpan? IRequestConfiguration.MaxRetryTimeout => _maxRetryTimeout; + + string? IRequestConfiguration.OpaqueId => _opaqueId; + + bool? IRequestConfiguration.ParseAllHeaders => _parseAllHeaders; + + TimeSpan? IRequestConfiguration.PingTimeout => _pingTimeout; + + TimeSpan? IRequestConfiguration.RequestTimeout => _requestTimeout; + + HeadersList? IRequestConfiguration.ResponseHeadersToParse => _responseHeadersToParse; + + string? IRequestConfiguration.RunAs => _runAs; + + bool? IRequestConfiguration.ThrowExceptions => _throwExceptions; + + bool? IRequestConfiguration.TransferEncodingChunked => _transferEncodingChunked; + + NameValueCollection? IRequestConfiguration.Headers => _headers; + + bool? IRequestConfiguration.EnableTcpStats => _enableTcpStats; + + bool? IRequestConfiguration.EnableThreadPoolStats => _enableThreadPoolStats; + + RequestMetaData? IRequestConfiguration.RequestMetaData => _requestMetaData; } diff --git a/src/Elastic.Transport/Configuration/TransportConfiguration.cs b/src/Elastic.Transport/Configuration/TransportConfiguration.cs index 72abbe2..db9c150 100644 --- a/src/Elastic.Transport/Configuration/TransportConfiguration.cs +++ b/src/Elastic.Transport/Configuration/TransportConfiguration.cs @@ -4,19 +4,11 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -#if !NETFRAMEWORK -using System.Net.Http; -#endif using System.Net.Security; -using System.Security; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Threading; -using Elastic.Transport.Extensions; using Elastic.Transport.Products; namespace Elastic.Transport; @@ -24,7 +16,7 @@ namespace Elastic.Transport; /// /// Allows you to control how behaves and where/how it connects to Elastic Stack products /// -public class TransportConfiguration : TransportConfigurationBase +public record TransportConfiguration : ITransportConfiguration { /// /// Detects whether we are running on .NET Core with CurlHandler. @@ -59,7 +51,7 @@ public class TransportConfiguration : TransportConfigurationBase - /// Creates a new instance of + /// Creates a new instance of /// /// The root of the Elastic stack product node we want to connect to. Defaults to http://localhost:9200 /// @@ -80,7 +72,7 @@ public TransportConfiguration(string cloudId, BasicAuthentication credentials, P public TransportConfiguration(string cloudId, Base64ApiKey credentials, ProductRegistration productRegistration = null) : this(new CloudNodePool(cloudId, credentials), productRegistration: productRegistration) { } - /// + /// /// /// /// @@ -89,403 +81,247 @@ public TransportConfiguration( NodePool nodePool, IRequestInvoker? invoker = null, Serializer? serializer = null, - ProductRegistration? productRegistration = null) - : base(nodePool, invoker, serializer, productRegistration) { } - -} - -/// > -[Browsable(false)] -[EditorBrowsable(EditorBrowsableState.Never)] -public abstract class TransportConfigurationBase : ITransportConfiguration - where T : TransportConfigurationBase -{ - private readonly IRequestInvoker _requestInvoker; - 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 Action _completedRequestHandler = DefaultCompletedRequestHandler; - private int _transportClientLimit; - private TimeSpan? _deadTimeout; - private bool _disableAutomaticProxyDetection; - private TimeSpan? _keepAliveInterval; - private TimeSpan? _keepAliveTime; - private TimeSpan? _maxDeadTimeout; - private Func _nodePredicate; - private Action _onRequestDataCreated = DefaultRequestDataCreated; - private string _proxyAddress; - private string _proxyPassword; - private string _proxyUsername; - private TimeSpan _dnsRefreshTimeout; - private Func _serverCertificateValidationCallback; - private IReadOnlyCollection _skipDeserializationForStatusCodes = new ReadOnlyCollection(new int[] { }); - private TimeSpan? _sniffLifeSpan; - private bool _sniffOnConnectionFault; - private bool _sniffOnStartup; - private MemoryStreamFactory _memoryStreamFactory; - private UserAgent _userAgent; - private string _certificateFingerprint; - private bool _disableMetaHeader; - private readonly MetaHeaderProvider? _metaHeaderProvider; - - private readonly Func _statusCodeToResponseSuccess; - - private IRequestConfiguration RequestConfig => this; - - /// - /// - /// - /// - /// - /// - /// - protected TransportConfigurationBase(NodePool nodePool, IRequestInvoker? requestInvoker, Serializer? requestResponseSerializer, ProductRegistration? productRegistration) + ProductRegistration? productRegistration = null + ) { - _nodePool = nodePool; - _requestInvoker = requestInvoker ?? new HttpRequestInvoker(); - _productRegistration = productRegistration ?? DefaultProductRegistration.Default; - - - UseThisRequestResponseSerializer = requestResponseSerializer ?? new LowLevelRequestResponseSerializer(); - RequestConfig.Accept = productRegistration?.DefaultMimeType; - - _transportClientLimit = TransportConfiguration.DefaultConnectionLimit; - _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 = Transport.UserAgent.Create(_productRegistration.Name, _productRegistration.GetType()); + //non init properties + NodePool = nodePool; + ProductRegistration = productRegistration ?? DefaultProductRegistration.Default; + Connection = invoker ?? new HttpRequestInvoker(); + Accept = productRegistration?.DefaultMimeType; + RequestResponseSerializer = serializer ?? new LowLevelRequestResponseSerializer(); + + ConnectionLimit = DefaultConnectionLimit; + DnsRefreshTimeout = DefaultDnsRefreshTimeout; + MemoryStreamFactory = DefaultMemoryStreamFactory; + SniffsOnConnectionFault = true; + SniffsOnStartup = true; + SniffInformationLifeSpan = TimeSpan.FromHours(1); + + MetaHeaderProvider = productRegistration?.MetaHeaderProvider; + UrlFormatter = new UrlFormatter(this); + StatusCodeToResponseSuccess = ProductRegistration.HttpStatusCodeClassifier; + UserAgent = UserAgent.Create(ProductRegistration.Name, ProductRegistration.GetType()); if (nodePool is CloudNodePool cloudPool) { - RequestConfig.Authentication = cloudPool.AuthenticationHeader; - RequestConfig.EnableHttpCompression = true; + Authentication = cloudPool.AuthenticationHeader; + EnableHttpCompression = true; } - RequestConfig.ResponseHeadersToParse = new HeadersList(_productRegistration.ResponseHeadersToParse); + ResponseHeadersToParse = 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; } - - string IRequestConfiguration.Accept { get; set; } - - IReadOnlyCollection IRequestConfiguration.AllowedStatusCodes { get; set; } - - AuthorizationHeader IRequestConfiguration.Authentication { get; set; } - SemaphoreSlim ITransportConfiguration.BootstrapLock => _semaphore; - X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; } - - string IRequestConfiguration.ContentType + /// Expert usage: Create a new transport configuration based of a previously configured instance + public TransportConfiguration(ITransportConfiguration config) { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + if (config is null) + throw new ArgumentNullException(nameof(config)); + + Accept = config.Accept; + AllowedStatusCodes = config.AllowedStatusCodes; + Authentication = config.Authentication; + BootstrapLock = config.BootstrapLock; + CertificateFingerprint = config.CertificateFingerprint; + ClientCertificates = config.ClientCertificates; + Connection = config.Connection; + ConnectionLimit = config.ConnectionLimit; + ContentType = config.ContentType; + DeadTimeout = config.DeadTimeout; + DisableAuditTrail = config.DisableAuditTrail; + DisableAutomaticProxyDetection = config.DisableAutomaticProxyDetection; + DisableDirectStreaming = config.DisableDirectStreaming; + DisableMetaHeader = config.DisableMetaHeader; + DisablePings = config.DisablePings; + DisableSniff = config.DisableSniff; + DnsRefreshTimeout = config.DnsRefreshTimeout; + EnableHttpCompression = config.EnableHttpCompression; + EnableTcpStats = config.EnableTcpStats; + EnableThreadPoolStats = config.EnableThreadPoolStats; + ForceNode = config.ForceNode; + Headers = config.Headers; + HttpPipeliningEnabled = config.HttpPipeliningEnabled; + KeepAliveInterval = config.KeepAliveInterval; + KeepAliveTime = config.KeepAliveTime; + MaxDeadTimeout = config.MaxDeadTimeout; + MaxRetries = config.MaxRetries; + MaxRetryTimeout = config.MaxRetryTimeout; + MemoryStreamFactory = config.MemoryStreamFactory; + NodePool = config.NodePool; + NodePredicate = config.NodePredicate; + OnRequestCompleted = config.OnRequestCompleted; + OnRequestDataCreated = config.OnRequestDataCreated; + OpaqueId = config.OpaqueId; + ParseAllHeaders = config.ParseAllHeaders; + PingTimeout = config.PingTimeout; + PrettyJson = config.PrettyJson; + ProductRegistration = config.ProductRegistration; + ProxyAddress = config.ProxyAddress; + ProxyPassword = config.ProxyPassword; + ProxyUsername = config.ProxyUsername; + QueryStringParameters = config.QueryStringParameters; + RequestMetaData = config.RequestMetaData; + RequestResponseSerializer = config.RequestResponseSerializer; + RequestTimeout = config.RequestTimeout; + ResponseHeadersToParse = config.ResponseHeadersToParse; + RunAs = config.RunAs; + ServerCertificateValidationCallback = config.ServerCertificateValidationCallback; + SkipDeserializationForStatusCodes = config.SkipDeserializationForStatusCodes; + SniffInformationLifeSpan = config.SniffInformationLifeSpan; + SniffsOnConnectionFault = config.SniffsOnConnectionFault; + SniffsOnStartup = config.SniffsOnStartup; + StatusCodeToResponseSuccess = config.StatusCodeToResponseSuccess; + ThrowExceptions = config.ThrowExceptions; + TransferEncodingChunked = config.TransferEncodingChunked; + UrlFormatter = config.UrlFormatter; + UserAgent = config.UserAgent; } - IRequestInvoker ITransportConfiguration.Connection => _requestInvoker; - ProductRegistration ITransportConfiguration.ProductRegistration => _productRegistration; - int ITransportConfiguration.ConnectionLimit => _transportClientLimit; - NodePool ITransportConfiguration.NodePool => _nodePool; - TimeSpan? ITransportConfiguration.DeadTimeout => _deadTimeout; - bool ITransportConfiguration.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; - bool? IRequestConfiguration.DisableDirectStreaming { get; set; } - bool? IRequestConfiguration.DisableAuditTrail { get; set; } - bool? IRequestConfiguration.DisablePings { get; set; } - - // TODO Assign ? - bool? IRequestConfiguration.DisableSniff { get; set; } - - bool? IRequestConfiguration.EnableHttpCompression { get; set; } - NameValueCollection IRequestConfiguration.Headers { get; set; } - bool? IRequestConfiguration.HttpPipeliningEnabled { get; set; } - - TimeSpan? ITransportConfiguration.KeepAliveInterval => _keepAliveInterval; - TimeSpan? ITransportConfiguration.KeepAliveTime => _keepAliveTime; - TimeSpan? ITransportConfiguration.MaxDeadTimeout => _maxDeadTimeout; - int? IRequestConfiguration.MaxRetries { get; set; } - TimeSpan? IRequestConfiguration.MaxRetryTimeout { get; set; } - - // never assigned globally - Uri? IRequestConfiguration.ForceNode { get; set; } - // never assigned globally - string IRequestConfiguration.OpaqueId { get; set; } - - MemoryStreamFactory ITransportConfiguration.MemoryStreamFactory => _memoryStreamFactory; - - Func ITransportConfiguration.NodePredicate => _nodePredicate; - Action ITransportConfiguration.OnRequestCompleted => _completedRequestHandler; - Action ITransportConfiguration.OnRequestDataCreated => _onRequestDataCreated; - TimeSpan? IRequestConfiguration.PingTimeout { get; set; } - string ITransportConfiguration.ProxyAddress => _proxyAddress; - string ITransportConfiguration.ProxyPassword => _proxyPassword; - string ITransportConfiguration.ProxyUsername => _proxyUsername; - NameValueCollection ITransportConfiguration.QueryStringParameters => _queryString; - Serializer ITransportConfiguration.RequestResponseSerializer => UseThisRequestResponseSerializer; - TimeSpan? IRequestConfiguration.RequestTimeout { get; set; } - 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; - - // TODO Assign - string IRequestConfiguration.RunAs { get; set; } - - bool? IRequestConfiguration.ThrowExceptions { get; set; } - UrlFormatter ITransportConfiguration.UrlFormatter => _urlFormatter; - UserAgent ITransportConfiguration.UserAgent => _userAgent; - Func ITransportConfiguration.StatusCodeToResponseSuccess => _statusCodeToResponseSuccess; - bool? IRequestConfiguration.TransferEncodingChunked { get; set; } - bool? IRequestConfiguration.EnableTcpStats { get; set; } - bool? IRequestConfiguration.EnableThreadPoolStats { get; set; } - - RequestMetaData? IRequestConfiguration.RequestMetaData - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - 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) => RequestConfig.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) => RequestConfig.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) => RequestConfig.ThrowExceptions = v); - - /// - public T DisablePing(bool disable = true) => Assign(disable, (a, v) => RequestConfig.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) => RequestConfig.RequestTimeout = v); - - /// - public T PingTimeout(TimeSpan timeout) => Assign(timeout, (a, v) => RequestConfig.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) => RequestConfig.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); - - /// - /// If your connection has to go through proxy, use this method to specify the proxy url - /// - public T Proxy(Uri proxyAddress) => - Assign(proxyAddress.ToString(), (a, v) => a._proxyAddress = v); - - /// - // ReSharper disable once MemberCanBePrivate.Global - public T DisableDirectStreaming(bool b = true) => Assign(b, (a, v) => RequestConfig.DisableDirectStreaming = v); - - /// - public T DisableAuditTrail(bool b = true) => Assign(b, (a, v) => RequestConfig.DisableAuditTrail = v); - - /// - public T OnRequestCompleted(Action handler) => - Assign(handler, (a, v) => a._completedRequestHandler += v ?? DefaultCompletedRequestHandler); - - /// - public T OnRequestDataCreated(Action handler) => - Assign(handler, (a, v) => a._onRequestDataCreated += v ?? DefaultRequestDataCreated); - - /// - public T Authentication(AuthorizationHeader header) => Assign(header, (a, v) => RequestConfig.Authentication = v); - - /// - public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, (a, v) => RequestConfig.HttpPipeliningEnabled = 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); - - bool? IRequestConfiguration.ParseAllHeaders { get; set; } - - /// - public virtual T ParseAllHeaders(bool b = true) => Assign(b, (a, v) => ((IRequestConfiguration)this).ParseAllHeaders = v); - - HeadersList? IRequestConfiguration.ResponseHeadersToParse { get; set; } - - MetaHeaderProvider ITransportConfiguration.MetaHeaderProvider => _metaHeaderProvider; - - bool ITransportConfiguration.DisableMetaHeader => _disableMetaHeader; - - /// - public virtual T ResponseHeadersToParse(HeadersList headersToParse) + public virtual bool DebugMode { - ((IRequestConfiguration)this).ResponseHeadersToParse = new HeadersList(((IRequestConfiguration)this).ResponseHeadersToParse, headersToParse); - return (T)this; + get => PrettyJson; + init + { + PrettyJson = value; + DisableDirectStreaming = value; + EnableTcpStats = value; + EnableThreadPoolStats = value; + } } - /// - public T ServerCertificateValidationCallback(Func callback) => - Assign(callback, (a, v) => a._serverCertificateValidationCallback = v); - - /// - public T ClientCertificates(X509CertificateCollection certificates) => - Assign(certificates, (a, v) => RequestConfig.ClientCertificates = v); - - /// - public T ClientCertificate(X509Certificate certificate) => - Assign(new X509Certificate2Collection { certificate }, (a, v) => RequestConfig.ClientCertificates = v); - - /// - public T ClientCertificate(string certificatePath) => - Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, (a, v) => RequestConfig.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) => RequestConfig.TransferEncodingChunked = v); - - /// - public T MemoryStreamFactory(MemoryStreamFactory memoryStreamFactory) => Assign(memoryStreamFactory, (a, v) => a._memoryStreamFactory = v); - - /// > - public T EnableTcpStats(bool enableTcpStats = true) => Assign(enableTcpStats, (a, v) => RequestConfig.EnableTcpStats = v); - - /// > - public T EnableThreadPoolStats(bool enableThreadPoolStats = true) => Assign(enableThreadPoolStats, (a, v) => RequestConfig.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() + /// + public NodePool NodePool { get; } + /// + public ProductRegistration ProductRegistration { get; } + /// + public SemaphoreSlim BootstrapLock { get; } = new(1, 1); + /// + public IRequestInvoker Connection { get; } + /// + public Serializer RequestResponseSerializer { get; } + + + /// + // ReSharper disable UnusedAutoPropertyAccessor.Global + public string? Accept { get; } + /// + public IReadOnlyCollection? AllowedStatusCodes { get; init; } + /// + public AuthorizationHeader? Authentication { get; init; } + /// + public X509CertificateCollection? ClientCertificates { get; init; } + /// + public string? ContentType { get; init; } + /// + public bool? DisableDirectStreaming { get; init; } + /// + public bool? DisableAuditTrail { get; init; } + /// + public bool? DisablePings { get; init; } + /// + public bool? DisableSniff { get; init; } + /// + public bool? HttpPipeliningEnabled { get; init; } + /// + public bool? EnableHttpCompression { get; init; } + /// + public Uri? ForceNode { get; init; } + /// + public int? MaxRetries { get; init; } + /// + public TimeSpan? MaxRetryTimeout { get; init; } + /// + public string? OpaqueId { get; init; } + /// + public bool? ParseAllHeaders { get; init; } + /// + public TimeSpan? PingTimeout { get; init; } + /// + public TimeSpan? RequestTimeout { get; init; } + /// + public HeadersList? ResponseHeadersToParse { get; init; } + /// + public string? RunAs { get; init; } + /// + public bool? ThrowExceptions { get; init; } + /// + public bool? TransferEncodingChunked { get; init; } + /// + public NameValueCollection? Headers { get; init; } + /// + public bool? EnableTcpStats { get; init; } + /// + public bool? EnableThreadPoolStats { get; init; } + /// + public RequestMetaData? RequestMetaData { get; init; } + + /// > + public void Dispose() { - _nodePool?.Dispose(); - _requestInvoker?.Dispose(); - _semaphore?.Dispose(); + NodePool.Dispose(); + Connection.Dispose(); + BootstrapLock.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; - } + /// + public int ConnectionLimit { get; init; } + /// + public TimeSpan? DeadTimeout { get; init; } + /// + public bool DisableAutomaticProxyDetection { get; init; } + /// + public TimeSpan? KeepAliveInterval { get; init; } + /// + public TimeSpan? KeepAliveTime { get; init; } + /// + public TimeSpan? MaxDeadTimeout { get; init; } + /// + public MemoryStreamFactory MemoryStreamFactory { get; init; } + /// + public Func? NodePredicate { get; init; } + /// + public Action? OnRequestCompleted { get; init; } + /// + public Action? OnRequestDataCreated { get; init; } + //TODO URI + /// + public string? ProxyAddress { get; init; } + /// + public string? ProxyPassword { get; init; } + /// + public string? ProxyUsername { get; init; } + /// + public NameValueCollection? QueryStringParameters { get; init; } + /// + public Func? ServerCertificateValidationCallback { get; init; } + /// + public string? CertificateFingerprint { get; init; } + /// + public IReadOnlyCollection? SkipDeserializationForStatusCodes { get; init; } + /// + public TimeSpan? SniffInformationLifeSpan { get; init; } + /// + public bool SniffsOnConnectionFault { get; init; } + /// + public bool SniffsOnStartup { get; init; } + /// + public UrlFormatter UrlFormatter { get; init; } + /// + public UserAgent UserAgent { get; init; } + /// + public Func StatusCodeToResponseSuccess { get; init; } + /// + public TimeSpan DnsRefreshTimeout { get; init; } + /// + public bool PrettyJson { get; init; } + /// + public MetaHeaderProvider? MetaHeaderProvider { get; init; } + /// + public bool DisableMetaHeader { get; init; } + // ReSharper restore UnusedAutoPropertyAccessor.Global } + diff --git a/src/Elastic.Transport/Configuration/TransportConfigurationDescriptor.cs b/src/Elastic.Transport/Configuration/TransportConfigurationDescriptor.cs new file mode 100644 index 0000000..73b2fca --- /dev/null +++ b/src/Elastic.Transport/Configuration/TransportConfigurationDescriptor.cs @@ -0,0 +1,462 @@ +// 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.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Elastic.Transport.Extensions; +using Elastic.Transport.Products; + +namespace Elastic.Transport; + +/// +/// Allows you to control how behaves and where/how it connects to Elastic Stack products +/// +public class TransportConfigurationDescriptor : TransportConfigurationDescriptorBase +{ + /// + /// Creates a new instance of + /// + /// The root of the Elastic stack product node we want to connect to. Defaults to http://localhost:9200 + /// + public TransportConfigurationDescriptor(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 TransportConfigurationDescriptor(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 TransportConfigurationDescriptor(string cloudId, Base64ApiKey credentials, ProductRegistration? productRegistration = null) + : this(new CloudNodePool(cloudId, credentials), productRegistration: productRegistration) { } + + /// + /// + /// + /// + /// + public TransportConfigurationDescriptor( + NodePool nodePool, + IRequestInvoker? invoker = null, + Serializer? serializer = null, + ProductRegistration? productRegistration = null) + : base(nodePool, invoker, serializer, productRegistration) { } + +} + +/// > +[Browsable(false)] +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class TransportConfigurationDescriptorBase : ITransportConfiguration + where T : TransportConfigurationDescriptorBase +{ + /// + /// + /// + /// + /// + /// + /// + protected TransportConfigurationDescriptorBase(NodePool nodePool, IRequestInvoker? requestInvoker, Serializer? requestResponseSerializer, ProductRegistration? productRegistration) + { + _nodePool = nodePool; + _productRegistration = productRegistration ?? DefaultProductRegistration.Default; + _connection = requestInvoker ?? new HttpRequestInvoker(); + _accept = productRegistration?.DefaultMimeType; + _bootstrapLock = new(1, 1); + + _requestResponseSerializer = requestResponseSerializer ?? new LowLevelRequestResponseSerializer(); + + _connectionLimit = TransportConfiguration.DefaultConnectionLimit; + _dnsRefreshTimeout = TransportConfiguration.DefaultDnsRefreshTimeout; + _memoryStreamFactory = TransportConfiguration.DefaultMemoryStreamFactory; + _sniffsOnConnectionFault = true; + _sniffsOnStartup = true; + _sniffInformationLifeSpan = TimeSpan.FromHours(1); + + _metaHeaderProvider = productRegistration?.MetaHeaderProvider; + + _urlFormatter = new UrlFormatter(this); + _statusCodeToResponseSuccess = _productRegistration.HttpStatusCodeClassifier; + _userAgent = Transport.UserAgent.Create(_productRegistration.Name, _productRegistration.GetType()); + + if (nodePool is CloudNodePool cloudPool) + { + _authentication = cloudPool.AuthenticationHeader; + _enableHttpCompression = true; + } + + _responseHeadersToParse = new HeadersList(_productRegistration.ResponseHeadersToParse); + } + + private readonly SemaphoreSlim _bootstrapLock; + private readonly IRequestInvoker _connection; + private readonly NodePool _nodePool; + private readonly ProductRegistration _productRegistration; + + //TODO these are not exposed globally +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private IReadOnlyCollection? _allowedStatusCodes; + private string? _contentType; + private bool? _disableSniff; + private Uri? _forceNode; + private string? _opaqueId; + private string? _runAs; + private RequestMetaData? _requestMetaData; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + + private bool _prettyJson; + private string? _accept; + private AuthorizationHeader? _authentication; + private X509CertificateCollection? _clientCertificates; + private bool? _disableDirectStreaming; + private bool? _disableAuditTrail; + private bool? _disablePings; + private bool? _httpPipeliningEnabled; + private bool? _enableHttpCompression; + private int? _maxRetries; + private TimeSpan? _maxRetryTimeout; + private TimeSpan? _pingTimeout; + private TimeSpan? _requestTimeout; + private bool? _throwExceptions; + private bool? _transferEncodingChunked; + private NameValueCollection? _headers; + private bool? _enableTcpStats; + private bool? _enableThreadPoolStats; + private int _connectionLimit; + private TimeSpan? _deadTimeout; + private bool _disableAutomaticProxyDetection; + private TimeSpan? _keepAliveInterval; + private TimeSpan? _keepAliveTime; + private TimeSpan? _maxDeadTimeout; + private MemoryStreamFactory _memoryStreamFactory; + private Func? _nodePredicate; + private Action? _onRequestCompleted; + private Action? _onRequestDataCreated; + private string? _proxyAddress; + private string? _proxyPassword; + private string? _proxyUsername; + private NameValueCollection? _queryStringParameters; + private Serializer _requestResponseSerializer; + private Func? _serverCertificateValidationCallback; + private string? _certificateFingerprint; + private IReadOnlyCollection? _skipDeserializationForStatusCodes; + private TimeSpan? _sniffInformationLifeSpan; + private bool _sniffsOnConnectionFault; + private bool _sniffsOnStartup; + private UrlFormatter _urlFormatter; + private UserAgent _userAgent; + private Func _statusCodeToResponseSuccess; + private TimeSpan _dnsRefreshTimeout; + private bool _disableMetaHeader; + private MetaHeaderProvider? _metaHeaderProvider; + private HeadersList? _responseHeadersToParse; + private bool? _parseAllHeaders; + + SemaphoreSlim ITransportConfiguration.BootstrapLock => _bootstrapLock; + IRequestInvoker ITransportConfiguration.Connection => _connection; + int ITransportConfiguration.ConnectionLimit => _connectionLimit; + NodePool ITransportConfiguration.NodePool => _nodePool; + ProductRegistration ITransportConfiguration.ProductRegistration => _productRegistration; + TimeSpan? ITransportConfiguration.DeadTimeout => _deadTimeout; + bool ITransportConfiguration.DisableAutomaticProxyDetection => _disableAutomaticProxyDetection; + TimeSpan? ITransportConfiguration.KeepAliveInterval => _keepAliveInterval; + TimeSpan? ITransportConfiguration.KeepAliveTime => _keepAliveTime; + TimeSpan? ITransportConfiguration.MaxDeadTimeout => _maxDeadTimeout; + MemoryStreamFactory ITransportConfiguration.MemoryStreamFactory => _memoryStreamFactory; + Func? ITransportConfiguration.NodePredicate => _nodePredicate; + Action? ITransportConfiguration.OnRequestCompleted => _onRequestCompleted; + Action? ITransportConfiguration.OnRequestDataCreated => _onRequestDataCreated; + string? ITransportConfiguration.ProxyAddress => _proxyAddress; + string? ITransportConfiguration.ProxyPassword => _proxyPassword; + string? ITransportConfiguration.ProxyUsername => _proxyUsername; + NameValueCollection? ITransportConfiguration.QueryStringParameters => _queryStringParameters; + Serializer ITransportConfiguration.RequestResponseSerializer => _requestResponseSerializer; + Func? ITransportConfiguration.ServerCertificateValidationCallback => _serverCertificateValidationCallback; + string? ITransportConfiguration.CertificateFingerprint => _certificateFingerprint; + IReadOnlyCollection? ITransportConfiguration.SkipDeserializationForStatusCodes => _skipDeserializationForStatusCodes; + TimeSpan? ITransportConfiguration.SniffInformationLifeSpan => _sniffInformationLifeSpan; + bool ITransportConfiguration.SniffsOnConnectionFault => _sniffsOnConnectionFault; + bool ITransportConfiguration.SniffsOnStartup => _sniffsOnStartup; + UrlFormatter ITransportConfiguration.UrlFormatter => _urlFormatter; + UserAgent ITransportConfiguration.UserAgent => _userAgent; + Func ITransportConfiguration.StatusCodeToResponseSuccess => _statusCodeToResponseSuccess; + TimeSpan ITransportConfiguration.DnsRefreshTimeout => _dnsRefreshTimeout; + bool ITransportConfiguration.PrettyJson => _prettyJson; + + + HeadersList? IRequestConfiguration.ResponseHeadersToParse => _responseHeadersToParse; + string? IRequestConfiguration.RunAs => _runAs; + bool? IRequestConfiguration.ThrowExceptions => _throwExceptions; + bool? IRequestConfiguration.TransferEncodingChunked => _transferEncodingChunked; + NameValueCollection? IRequestConfiguration.Headers => _headers; + bool? IRequestConfiguration.EnableTcpStats => _enableTcpStats; + bool? IRequestConfiguration.EnableThreadPoolStats => _enableThreadPoolStats; + RequestMetaData? IRequestConfiguration.RequestMetaData => _requestMetaData; + MetaHeaderProvider? ITransportConfiguration.MetaHeaderProvider => _metaHeaderProvider; + bool ITransportConfiguration.DisableMetaHeader => _disableMetaHeader; + string? IRequestConfiguration.Accept => _accept; + IReadOnlyCollection? IRequestConfiguration.AllowedStatusCodes => _allowedStatusCodes; + AuthorizationHeader? IRequestConfiguration.Authentication => _authentication; + X509CertificateCollection? IRequestConfiguration.ClientCertificates => _clientCertificates; + string? IRequestConfiguration.ContentType => _contentType; + bool? IRequestConfiguration.DisableDirectStreaming => _disableDirectStreaming; + bool? IRequestConfiguration.DisableAuditTrail => _disableAuditTrail; + bool? IRequestConfiguration.DisablePings => _disablePings; + bool? IRequestConfiguration.DisableSniff => _disableSniff; + bool? IRequestConfiguration.HttpPipeliningEnabled => _httpPipeliningEnabled; + bool? IRequestConfiguration.EnableHttpCompression => _enableHttpCompression; + Uri? IRequestConfiguration.ForceNode => _forceNode; + int? IRequestConfiguration.MaxRetries => _maxRetries; + TimeSpan? IRequestConfiguration.MaxRetryTimeout => _maxRetryTimeout; + string? IRequestConfiguration.OpaqueId => _opaqueId; + bool? IRequestConfiguration.ParseAllHeaders => _parseAllHeaders; + TimeSpan? IRequestConfiguration.PingTimeout => _pingTimeout; + TimeSpan? IRequestConfiguration.RequestTimeout => _requestTimeout; + + + /// + /// 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 => _requestResponseSerializer; + set => _requestResponseSerializer = value; + } + + 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, static (a, v) => a._keepAliveTime = v) + .Assign(keepAliveInterval, static (a, v) => a._keepAliveInterval = v); + + /// + public T MaximumRetries(int maxRetries) => Assign(maxRetries, static (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, static (a, v) => a._connectionLimit = v); + + /// + public T SniffOnConnectionFault(bool sniffsOnConnectionFault = true) => + Assign(sniffsOnConnectionFault, static (a, v) => a._sniffsOnConnectionFault = v); + + /// + public T SniffOnStartup(bool sniffsOnStartup = true) => Assign(sniffsOnStartup, static (a, v) => a._sniffsOnStartup = v); + + /// + /// + /// + /// The duration a clusterstate is considered fresh, set to null to disable periodic sniffing + public T SniffLifeSpan(TimeSpan? sniffLifeSpan) => Assign(sniffLifeSpan, static (a, v) => a._sniffInformationLifeSpan = v); + + /// + public T EnableHttpCompression(bool enabled = true) => Assign(enabled, static (a, v) => a._enableHttpCompression = v); + + /// + public T DisableAutomaticProxyDetection(bool disable = true) => Assign(disable, static (a, v) => a._disableAutomaticProxyDetection = v); + + /// + public T ThrowExceptions(bool alwaysThrow = true) => Assign(alwaysThrow, static (a, v) => a._throwExceptions = v); + + /// + public T DisablePing(bool disable = true) => Assign(disable, static (a, v) => a._disablePings = v); + + /// + // ReSharper disable once MemberCanBePrivate.Global + public T GlobalQueryStringParameters(NameValueCollection queryStringParameters) => Assign(queryStringParameters, static (a, v) => + { + a._queryStringParameters ??= new(); + a._queryStringParameters.Add(v); + }); + + /// + public T GlobalHeaders(NameValueCollection headers) => Assign(headers, static (a, v) => + { + a._headers ??= new(); + a._headers.Add(v); + }); + + /// + public T RequestTimeout(TimeSpan timeout) => Assign(timeout, static (a, v) => a._requestTimeout = v); + + /// + public T PingTimeout(TimeSpan timeout) => Assign(timeout, static (a, v) => a._pingTimeout = v); + + /// + public T DeadTimeout(TimeSpan timeout) => Assign(timeout, static (a, v) => a._deadTimeout = v); + + /// + public T MaxDeadTimeout(TimeSpan timeout) => Assign(timeout, static (a, v) => a._maxDeadTimeout = v); + + /// + public T MaxRetryTimeout(TimeSpan maxRetryTimeout) => Assign(maxRetryTimeout, static (a, v) => a._maxRetryTimeout = v); + + /// + public T DnsRefreshTimeout(TimeSpan timeout) => Assign(timeout, static (a, v) => a._dnsRefreshTimeout = v); + + /// + public T CertificateFingerprint(string fingerprint) => Assign(fingerprint, static (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(), static (a, v) => a._proxyAddress = v) + .Assign(username, static (a, v) => a._proxyUsername = v) + .Assign(password, static (a, v) => a._proxyPassword = v); + + /// + /// If your connection has to go through proxy, use this method to specify the proxy url + /// + public T Proxy(Uri proxyAddress) => + Assign(proxyAddress.ToString(), static (a, v) => a._proxyAddress = v); + + /// + // ReSharper disable once MemberCanBePrivate.Global + public T DisableDirectStreaming(bool b = true) => Assign(b, static (a, v) => a._disableDirectStreaming = v); + + /// + public T DisableAuditTrail(bool b = true) => Assign(b, static (a, v) => a._disableAuditTrail = v); + + /// + public T OnRequestCompleted(Action handler) => + Assign(handler, static (a, v) => a._onRequestCompleted += v ?? DefaultCompletedRequestHandler); + + /// + public T OnRequestDataCreated(Action handler) => + Assign(handler, static (a, v) => a._onRequestDataCreated += v ?? DefaultRequestDataCreated); + + /// + public T Authentication(AuthorizationHeader header) => Assign(header, static (a, v) => a._authentication = v); + + /// + public T EnableHttpPipelining(bool enabled = true) => Assign(enabled, static (a, v) => a._httpPipeliningEnabled = v); + + /// + /// + /// + /// Return true if you want the node to be used for API calls + public T NodePredicate(Func predicate) => Assign(predicate, static (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, static (a, v) => + a._onRequestCompleted += v ?? (d => Debug.WriteLine(d.DebugInformation))); + + /// + // ReSharper disable once VirtualMemberNeverOverridden.Global + // ReSharper disable once MemberCanBeProtected.Global + public virtual T PrettyJson(bool b = true) => Assign(b, static (a, v) => a._prettyJson = v); + + /// + public virtual T ParseAllHeaders(bool b = true) => Assign(b, static (a, v) => a._parseAllHeaders = v); + + /// + public virtual T ResponseHeadersToParse(HeadersList headersToParse) => + Assign(headersToParse, static (a, v) => a._responseHeadersToParse = new HeadersList(a._responseHeadersToParse, v)); + + /// + public T ServerCertificateValidationCallback(Func callback) => + Assign(callback, static (a, v) => a._serverCertificateValidationCallback = v); + + /// + public T ClientCertificates(X509CertificateCollection certificates) => + Assign(certificates, static (a, v) => a._clientCertificates = v); + + /// + public T ClientCertificate(X509Certificate certificate) => + Assign(new X509Certificate2Collection { certificate }, static (a, v) => a._clientCertificates = v); + + /// + public T ClientCertificate(string certificatePath) => + Assign(new X509Certificate2Collection { new X509Certificate(certificatePath) }, static (a, v) => a._clientCertificates = v); + + /// + public T SkipDeserializationForStatusCodes(params int[] statusCodes) => + Assign(new ReadOnlyCollection(statusCodes), static (a, v) => a._skipDeserializationForStatusCodes = v); + + /// + public T UserAgent(UserAgent userAgent) => Assign(userAgent, static (a, v) => a._userAgent = v); + + /// + public T TransferEncodingChunked(bool transferEncodingChunked = true) => Assign(transferEncodingChunked, static (a, v) => a._transferEncodingChunked = v); + + /// + public T MemoryStreamFactory(MemoryStreamFactory memoryStreamFactory) => Assign(memoryStreamFactory, static (a, v) => a._memoryStreamFactory = v); + + /// > + public T EnableTcpStats(bool enableTcpStats = true) => Assign(enableTcpStats, static (a, v) => a._enableTcpStats = v); + + /// > + public T EnableThreadPoolStats(bool enableThreadPoolStats = true) => Assign(enableThreadPoolStats, static (a, v) => a._enableThreadPoolStats = v); + + /// > + public T DisableMetaHeader(bool disable = true) => Assign(disable, static (a, v) => a._disableMetaHeader = v); + + // ReSharper disable once VirtualMemberNeverOverridden.Global + /// Allows subclasses to hook into the parents dispose + protected virtual void DisposeManagedResources() + { + _nodePool.Dispose(); + _connection.Dispose(); + _bootstrapLock.Dispose(); + } + + /// Allows subclasses to add/remove default global query string parameters + protected T UpdateGlobalQueryString(string key, string value, bool enabled) + { + _queryStringParameters ??= new(); + if (!enabled && _queryStringParameters[key] != null) _queryStringParameters.Remove(key); + else if (enabled && _queryStringParameters[key] == null) + return GlobalQueryStringParameters(new NameValueCollection { { key, "true" } }); + return (T)this; + } + + void IDisposable.Dispose() => DisposeManagedResources(); + +} diff --git a/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs index a7ea8e1..f9dc741 100644 --- a/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/AuditDiagnosticObserver.cs @@ -8,7 +8,7 @@ namespace Elastic.Transport.Diagnostics; -/// Provides a typed listener to events that emits +/// Provides a typed listener to events that emits internal sealed class AuditDiagnosticObserver : TypedDiagnosticObserver { /// diff --git a/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs b/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs index a619157..77f15a1 100644 --- a/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs +++ b/src/Elastic.Transport/Diagnostics/RequestPipelineDiagnosticObserver.cs @@ -7,7 +7,7 @@ namespace Elastic.Transport.Diagnostics; -/// Provides a typed listener to actions that takes e.g sniff, ping, or making an API call ; +/// Provides a typed listener to actions that takes e.g sniff, ping, or making an API call ; internal sealed class RequestPipelineDiagnosticObserver : TypedDiagnosticObserver { /// diff --git a/src/Elastic.Transport/DistributedTransport.cs b/src/Elastic.Transport/DistributedTransport.cs index c5d1b16..c24dfc0 100644 --- a/src/Elastic.Transport/DistributedTransport.cs +++ b/src/Elastic.Transport/DistributedTransport.cs @@ -20,7 +20,7 @@ namespace Elastic.Transport; /// -public sealed class DistributedTransport : DistributedTransport +public sealed class DistributedTransport : DistributedTransport { /// /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on @@ -28,7 +28,7 @@ public sealed class DistributedTransport : DistributedTransport /// The configuration to use for this transport - public DistributedTransport(TransportConfiguration configurationValues) : base(configurationValues, null, null) { } + public DistributedTransport(ITransportConfiguration configurationValues) : base(configurationValues, null, null) { } /// /// Transport coordinates the client requests over the node pool nodes and is in charge of falling over on @@ -38,14 +38,12 @@ public DistributedTransport(TransportConfiguration configurationValues) : base(c /// The configuration 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 DistributedTransport( - TransportConfiguration configurationValues, - RequestPipelineFactory? pipelineProvider = null, - DateTimeProvider? dateTimeProvider = null, - MemoryStreamFactory? memoryStreamFactory = null + ITransportConfiguration configurationValues, + RequestPipelineFactory? pipelineProvider = null, + DateTimeProvider? dateTimeProvider = null ) - : base(configurationValues, pipelineProvider, dateTimeProvider, memoryStreamFactory) { } + : base(configurationValues, pipelineProvider, dateTimeProvider) { } } /// @@ -62,12 +60,10 @@ public class DistributedTransport : ITransport /// The configuration 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 DistributedTransport( TConfiguration configurationValues, - RequestPipelineFactory? pipelineProvider = null, - DateTimeProvider? dateTimeProvider = null, - MemoryStreamFactory? memoryStreamFactory = null + RequestPipelineFactory? pipelineProvider = null, + DateTimeProvider? dateTimeProvider = null ) { configurationValues.ThrowIfNull(nameof(configurationValues)); @@ -78,14 +74,16 @@ public DistributedTransport( _productRegistration = configurationValues.ProductRegistration; Configuration = configurationValues; - PipelineProvider = pipelineProvider ?? new DefaultRequestPipelineFactory(); + TransportRequestData = new RequestData(Configuration); + PipelineProvider = pipelineProvider ?? new DefaultRequestPipelineFactory(); DateTimeProvider = dateTimeProvider ?? DefaultDateTimeProvider.Default; - MemoryStreamFactory = memoryStreamFactory ?? configurationValues.MemoryStreamFactory; + MemoryStreamFactory = configurationValues.MemoryStreamFactory; } private DateTimeProvider DateTimeProvider { get; } private MemoryStreamFactory MemoryStreamFactory { get; } - private RequestPipelineFactory PipelineProvider { get; } + private RequestPipelineFactory PipelineProvider { get; } + private RequestData TransportRequestData { get; } /// public TConfiguration Configuration { get; } @@ -120,7 +118,7 @@ private async ValueTask RequestCoreAsync( EndpointPath path, PostData? data, OpenTelemetryData openTelemetryData, - IRequestConfiguration? localRequestConfiguration, + IRequestConfiguration? localConfiguration, CustomResponseBuilder? customResponseBuilder, CancellationToken cancellationToken = default ) @@ -134,16 +132,22 @@ private async ValueTask RequestCoreAsync( try { - using var pipeline = PipelineProvider.Create(Configuration, DateTimeProvider, MemoryStreamFactory, localRequestConfiguration); + //unless per request configuration or custom response builder is provided we can reuse a request data + //that is specific to this transport + var requestData = + localConfiguration != null || customResponseBuilder != null + ? new RequestData(Configuration, localConfiguration, customResponseBuilder) + : TransportRequestData; + + Configuration.OnRequestDataCreated?.Invoke(requestData); + + using var pipeline = PipelineProvider.Create(requestData, DateTimeProvider); if (isAsync) await pipeline.FirstPoolUsageAsync(Configuration.BootstrapLock, cancellationToken).ConfigureAwait(false); else pipeline.FirstPoolUsage(Configuration.BootstrapLock); - //var pathAndQuery = requestParameters?.CreatePathWithQueryStrings(path, Configuration) ?? path; - var requestData = new RequestData(Configuration, localRequestConfiguration, customResponseBuilder, MemoryStreamFactory); - Configuration.OnRequestDataCreated?.Invoke(requestData); TResponse response = null; var endpoint = Endpoint.Empty(path); diff --git a/src/Elastic.Transport/ITransportHttpMethodExtensions.cs b/src/Elastic.Transport/ITransportHttpMethodExtensions.cs index e987d48..26b86aa 100644 --- a/src/Elastic.Transport/ITransportHttpMethodExtensions.cs +++ b/src/Elastic.Transport/ITransportHttpMethodExtensions.cs @@ -17,12 +17,12 @@ private static EndpointPath ToEndpointPath(HttpMethod method, string path, Reque new(method, parameters.CreatePathWithQueryStrings(path, configuration)); /// Perform a GET request - public static TResponse Get(this ITransport> transport, string path, RequestParameters parameters) + public static TResponse Get(this ITransport transport, string path, RequestParameters parameters) where TResponse : TransportResponse, new() => transport.Request(ToEndpointPath(GET, path, parameters, transport.Configuration), postData: null, openTelemetryData: default, null, null); /// Perform a GET request - public static Task GetAsync(this ITransport> transport, string path, + public static Task GetAsync(this ITransport transport, string path, RequestParameters parameters, CancellationToken cancellationToken = default) where TResponse : TransportResponse, new() => transport.RequestAsync(ToEndpointPath(GET, path, parameters, transport.Configuration), postData: null, openTelemetryData: default, null, null, cancellationToken); @@ -38,11 +38,11 @@ public static Task GetAsync(this ITransport transport, str transport.RequestAsync(new EndpointPath(GET, pathAndQuery), postData: null, openTelemetryData: default, null, null, cancellationToken); /// Perform a HEAD request - public static VoidResponse Head(this ITransport> transport, string path, RequestParameters parameters) + public static VoidResponse Head(this ITransport transport, string path, RequestParameters parameters) => transport.Request(ToEndpointPath(HEAD, path, parameters, transport.Configuration), postData: null, openTelemetryData: default, null, null); /// Perform a HEAD request - public static Task HeadAsync(this ITransport> transport, string path, RequestParameters parameters, CancellationToken cancellationToken = default) + public static Task HeadAsync(this ITransport transport, string path, RequestParameters parameters, CancellationToken cancellationToken = default) => transport.RequestAsync(ToEndpointPath(HEAD, path, parameters, transport.Configuration), postData: null, openTelemetryData: default, null, null, cancellationToken); /// Perform a HEAD request @@ -54,12 +54,12 @@ public static Task HeadAsync(this ITransport transport, string pat => transport.RequestAsync(new EndpointPath(HEAD, pathAndQuery), postData: null, openTelemetryData: default, null, null, cancellationToken); /// Perform a POST request - public static TResponse Post(this ITransport> transport, string path, PostData data, RequestParameters parameters) + public static TResponse Post(this ITransport transport, string path, PostData data, RequestParameters parameters) where TResponse : TransportResponse, new() => transport.Request(ToEndpointPath(POST, path, parameters, transport.Configuration), data, openTelemetryData: default, null, null); /// Perform a POST request - public static Task PostAsync(this ITransport> transport, string path, PostData data, + public static Task PostAsync(this ITransport transport, string path, PostData data, RequestParameters parameters, CancellationToken cancellationToken = default) where TResponse : TransportResponse, new() => transport.RequestAsync(ToEndpointPath(POST, path, parameters, transport.Configuration), data, openTelemetryData: default, null, null, cancellationToken); @@ -75,12 +75,12 @@ public static Task PostAsync(this ITransport transport, st transport.RequestAsync(new EndpointPath(POST, pathAndQuery), data, openTelemetryData: default, null, null, cancellationToken); /// Perform a PUT request - public static TResponse Put(this ITransport> transport, string path, PostData data, RequestParameters parameters) + public static TResponse Put(this ITransport transport, string path, PostData data, RequestParameters parameters) where TResponse : TransportResponse, new() => transport.Request(ToEndpointPath(PUT, path, parameters, transport.Configuration), data, openTelemetryData: default, null, null); /// Perform a PUT request - public static Task PutAsync(this ITransport> transport, string path, PostData data, RequestParameters parameters, CancellationToken cancellationToken = default) + public static Task PutAsync(this ITransport transport, string path, PostData data, RequestParameters parameters, CancellationToken cancellationToken = default) where TResponse : TransportResponse, new() => transport.RequestAsync(ToEndpointPath(PUT, path, parameters, transport.Configuration), data, openTelemetryData: default, null, null, cancellationToken); @@ -96,12 +96,12 @@ public static Task PutAsync(this ITransport transport, str /// Perform a DELETE request - public static TResponse Delete(this ITransport> transport, string path, RequestParameters parameters, PostData? data = null) + public static TResponse Delete(this ITransport transport, string path, RequestParameters parameters, PostData? data = null) where TResponse : TransportResponse, new() => transport.Request(ToEndpointPath(DELETE, path, parameters, transport.Configuration), data, openTelemetryData: default, null, null); /// Perform a DELETE request - public static Task DeleteAsync(this ITransport> transport, string path, RequestParameters parameters, PostData? data = null, CancellationToken cancellationToken = default) + public static Task DeleteAsync(this ITransport transport, string path, RequestParameters parameters, PostData? data = null, CancellationToken cancellationToken = default) where TResponse : TransportResponse, new() => transport.RequestAsync(ToEndpointPath(DELETE, path, parameters, transport.Configuration), data, openTelemetryData: default, null, null, cancellationToken); diff --git a/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs b/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs index 5b2ff5d..7228490 100644 --- a/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs +++ b/src/Elastic.Transport/Requests/MetaData/DefaultMetaHeaderProvider.cs @@ -11,22 +11,20 @@ namespace Elastic.Transport; /// public sealed class DefaultMetaHeaderProvider : MetaHeaderProvider { - private readonly MetaHeaderProducer[] _producers; - /// - public override MetaHeaderProducer[] Producers => _producers; + public override MetaHeaderProducer[] Producers { get; } /// /// /// public DefaultMetaHeaderProvider(Type clientType, string serviceIdentifier) => - _producers = new MetaHeaderProducer[] { new DefaultMetaHeaderProducer(clientType, serviceIdentifier) }; + Producers = [new DefaultMetaHeaderProducer(clientType, serviceIdentifier)]; /// /// /// public DefaultMetaHeaderProvider(VersionInfo versionInfo, string serviceIdentifier) => - _producers = new MetaHeaderProducer[] { new DefaultMetaHeaderProducer(versionInfo, serviceIdentifier) }; + Producers = [new DefaultMetaHeaderProducer(versionInfo, serviceIdentifier)]; } /// diff --git a/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs b/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs deleted file mode 100644 index b2a90fc..0000000 --- a/src/Elastic.Transport/Requests/MetaData/RequestConfigurationExtensions.cs +++ /dev/null @@ -1,31 +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; - -/// -/// -/// -public static class RequestConfigurationExtensions -{ - /// - /// - /// - /// - /// - /// - public static void SetRequestMetaData(this IRequestConfiguration requestConfiguration, RequestMetaData requestMetaData) - { - if (requestConfiguration is null) - throw new ArgumentNullException(nameof(requestConfiguration)); - - if (requestMetaData is null) - throw new ArgumentNullException(nameof(requestMetaData)); - - requestConfiguration.RequestMetaData = requestMetaData; - } -} - diff --git a/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs b/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs index 551ea9f..4dfe57f 100644 --- a/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs +++ b/src/Elastic.Transport/Requests/MetaData/RequestMetaData.cs @@ -17,13 +17,13 @@ public sealed class RequestMetaData /// internal const string HelperKey = "helper"; - private Dictionary _metaDataItems; + private Dictionary? _metaDataItems; internal bool TryAddMetaData(string key, string value) { - _metaDataItems ??= new Dictionary(); + _metaDataItems ??= new(); -#if NETSTANDARD2_1 +#if NETSTANDARD2_1 || NET6_0_OR_GREATER return _metaDataItems.TryAdd(key, value); #else if (_metaDataItems.ContainsKey(key)) @@ -34,8 +34,6 @@ internal bool TryAddMetaData(string key, string value) #endif } - /// - /// - /// - public IReadOnlyDictionary Items => _metaDataItems ?? EmptyReadOnly.Dictionary; + /// Retrieves a read-only dictionary of metadata items associated with a client request. + public IReadOnlyDictionary Items => _metaDataItems; } diff --git a/tests/Elastic.Elasticsearch.IntegrationTests/DefaultCluster.cs b/tests/Elastic.Elasticsearch.IntegrationTests/DefaultCluster.cs index cd90989..55e6eb6 100644 --- a/tests/Elastic.Elasticsearch.IntegrationTests/DefaultCluster.cs +++ b/tests/Elastic.Elasticsearch.IntegrationTests/DefaultCluster.cs @@ -29,7 +29,7 @@ public ITransport CreateClient(ITestOutputHelper output) => { var nodes = NodesUris(); var nodePool = new StaticNodePool(nodes); - var settings = new TransportConfiguration(nodePool, productRegistration: ElasticsearchProductRegistration.Default) + var settings = new TransportConfigurationDescriptor(nodePool, productRegistration: ElasticsearchProductRegistration.Default) .RequestTimeout(TimeSpan.FromSeconds(5)) .OnRequestCompleted(d => { diff --git a/tests/Elastic.Transport.IntegrationTests/Http/StreamResponseTests.cs b/tests/Elastic.Transport.IntegrationTests/Http/StreamResponseTests.cs index 2b4084c..1e8d2a0 100644 --- a/tests/Elastic.Transport.IntegrationTests/Http/StreamResponseTests.cs +++ b/tests/Elastic.Transport.IntegrationTests/Http/StreamResponseTests.cs @@ -39,8 +39,10 @@ public async Task StreamResponse_MemoryStreamShouldNotBeDisposed() var nodePool = new SingleNodePool(Server.Uri); var memoryStreamFactory = new TrackMemoryStreamFactory(); var config = new TransportConfiguration(nodePool, productRegistration: new ElasticsearchProductRegistration(typeof(Clients.Elasticsearch.ElasticsearchClient))) - .MemoryStreamFactory(memoryStreamFactory) - .DisableDirectStreaming(true); + { + MemoryStreamFactory = memoryStreamFactory, + DisableDirectStreaming = true + }; var transport = new DistributedTransport(config); @@ -57,7 +59,9 @@ public async Task StringResponse_MemoryStreamShouldBeDisposed() var nodePool = new SingleNodePool(Server.Uri); var memoryStreamFactory = new TrackMemoryStreamFactory(); var config = new TransportConfiguration(nodePool, productRegistration: new ElasticsearchProductRegistration(typeof(Clients.Elasticsearch.ElasticsearchClient))) - .MemoryStreamFactory(memoryStreamFactory); + { + MemoryStreamFactory = memoryStreamFactory + }; var transport = new DistributedTransport(config); @@ -65,9 +69,7 @@ public async Task StringResponse_MemoryStreamShouldBeDisposed() memoryStreamFactory.Created.Count.Should().Be(2); foreach (var memoryStream in memoryStreamFactory.Created) - { memoryStream.IsDisposed.Should().BeTrue(); - } } [Fact] @@ -76,8 +78,10 @@ public async Task WhenInvalidJson_MemoryStreamShouldBeDisposed() var nodePool = new SingleNodePool(Server.Uri); var memoryStreamFactory = new TrackMemoryStreamFactory(); var config = new TransportConfiguration(nodePool, productRegistration: new ElasticsearchProductRegistration(typeof(Clients.Elasticsearch.ElasticsearchClient))) - .MemoryStreamFactory(memoryStreamFactory) - .DisableDirectStreaming(true); + { + MemoryStreamFactory = memoryStreamFactory, + DisableDirectStreaming = true + }; var transport = new DistributedTransport(config); @@ -86,9 +90,7 @@ public async Task WhenInvalidJson_MemoryStreamShouldBeDisposed() memoryStreamFactory.Created.Count.Should().Be(3); foreach (var memoryStream in memoryStreamFactory.Created) - { memoryStream.IsDisposed.Should().BeTrue(); - } } [Fact] @@ -97,7 +99,9 @@ public async Task WhenNoContent_MemoryStreamShouldBeDisposed() var nodePool = new SingleNodePool(Server.Uri); var memoryStreamFactory = new TrackMemoryStreamFactory(); var config = new TransportConfiguration(nodePool, productRegistration: new ElasticsearchProductRegistration(typeof(Clients.Elasticsearch.ElasticsearchClient))) - .MemoryStreamFactory(memoryStreamFactory); + { + MemoryStreamFactory = memoryStreamFactory, + }; var transport = new DistributedTransport(config); @@ -117,8 +121,10 @@ public async Task PlainText_MemoryStreamShouldBeDisposed() var nodePool = new SingleNodePool(Server.Uri); var memoryStreamFactory = new TrackMemoryStreamFactory(); var config = new TransportConfiguration(nodePool, productRegistration: new ElasticsearchProductRegistration(typeof(Clients.Elasticsearch.ElasticsearchClient))) - .MemoryStreamFactory(memoryStreamFactory) - .DisableDirectStreaming(true); + { + MemoryStreamFactory = memoryStreamFactory, + DisableDirectStreaming = true + }; var transport = new DistributedTransport(config); @@ -127,14 +133,10 @@ public async Task PlainText_MemoryStreamShouldBeDisposed() memoryStreamFactory.Created.Count.Should().Be(3); foreach (var memoryStream in memoryStreamFactory.Created) - { memoryStream.IsDisposed.Should().BeTrue(); - } } - private class TestResponse : TransportResponse - { - } + private class TestResponse : TransportResponse; private class TrackDisposeStream : MemoryStream { diff --git a/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs b/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs index e167fcf..7891ba0 100644 --- a/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs +++ b/tests/Elastic.Transport.IntegrationTests/Http/TransferEncodingChunckedTests.cs @@ -40,10 +40,12 @@ private ITransport Setup( { var nodePool = new SingleNodePool(Server.Uri); var config = new TransportConfiguration(nodePool, requestInvoker) - .TransferEncodingChunked(transferEncodingChunked) - .EnableHttpCompression(httpCompression); + { + TransferEncodingChunked = transferEncodingChunked, + EnableHttpCompression = httpCompression + }; config = disableAutomaticProxyDetection.HasValue - ? config.DisableAutomaticProxyDetection(disableAutomaticProxyDetection.Value) + ? config with { DisableAutomaticProxyDetection = disableAutomaticProxyDetection.Value } //make sure we the requests in debugging proxy : TransportTestServer.RerouteToProxyIfNeeded(config); diff --git a/tests/Elastic.Transport.IntegrationTests/OpenTelemetry/OpenTelemetryTests.cs b/tests/Elastic.Transport.IntegrationTests/OpenTelemetry/OpenTelemetryTests.cs index 500af63..87fbc26 100644 --- a/tests/Elastic.Transport.IntegrationTests/OpenTelemetry/OpenTelemetryTests.cs +++ b/tests/Elastic.Transport.IntegrationTests/OpenTelemetry/OpenTelemetryTests.cs @@ -39,7 +39,7 @@ public async Task ElasticsearchTagsShouldBeSet_WhenUsingTheElasticsearchRegistra var mre = new ManualResetEvent(false); var callCounter = 0; - using var listener = new ActivityListener() + using var listener = new ActivityListener { ActivityStarted = _ => { }, ActivityStopped = activity => diff --git a/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs b/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs index a920b7c..96a26f2 100644 --- a/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs +++ b/tests/Elastic.Transport.IntegrationTests/Plumbing/TransportTestServer.cs @@ -33,7 +33,7 @@ public static TransportConfiguration RerouteToProxyIfNeeded(TransportConfigurati { if (!RunningMitmProxy) return config; - return config.Proxy(new Uri("http://127.0.0.1:8080"), null, (string)null); + return config with { ProxyAddress = "http://127.0.0.1:8080" }; } } diff --git a/tests/Elastic.Transport.Tests/Configuration/TransportConfigurationTests.cs b/tests/Elastic.Transport.Tests/Configuration/TransportConfigurationTests.cs new file mode 100644 index 0000000..e6ce796 --- /dev/null +++ b/tests/Elastic.Transport.Tests/Configuration/TransportConfigurationTests.cs @@ -0,0 +1,60 @@ +// 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.Security.Cryptography.X509Certificates; +using System.Threading; +using FluentAssertions; +using Xunit; +#if !NETFRAMEWORK +using Soenneker.Utils.AutoBogus; +#endif + +namespace Elastic.Transport.Tests.Configuration; + +public class TransportConfigurationTests +{ + [Fact] + public void CopiesAllDefaults() + { + var config = new TransportConfiguration(); + var newConfig = new TransportConfiguration(config); + + config.Should().BeEquivalentTo(newConfig); + } + + [Fact] + public void SameDefaults() + { + ITransportConfiguration config = new TransportConfiguration(); + ITransportConfiguration newConfig = new TransportConfigurationDescriptor(); + + config.Should().BeEquivalentTo(newConfig, c => c + .Excluding(p=>p.BootstrapLock) + .Excluding(p=>p.NodePool.LastUpdate) + ); + + config.BootstrapLock.CurrentCount.Should().Be(newConfig.BootstrapLock.CurrentCount); + config.NodePool.LastUpdate + .Should().BeCloseTo(newConfig.NodePool.LastUpdate, TimeSpan.FromSeconds(2)); + } + +#if !NETFRAMEWORK + [Fact] + public void CopiesAllProperties() + { + var autoFaker = new AutoFaker(); + autoFaker.RuleFor(x => x.BootstrapLock, f => new SemaphoreSlim(1, 1)); + autoFaker.RuleFor(x => x.ClientCertificates, f => new X509CertificateCollection()); + + var config = autoFaker.Generate(); + var newConfig = new TransportConfiguration(config); + + config.Accept.Should().NotBeEmpty(); + config.ClientCertificates.Should().NotBeNull(); + + config.Should().BeEquivalentTo(newConfig); + } +#endif +} diff --git a/tests/Elastic.Transport.Tests/Elastic.Transport.Tests.csproj b/tests/Elastic.Transport.Tests/Elastic.Transport.Tests.csproj index 3263035..688d7d8 100644 --- a/tests/Elastic.Transport.Tests/Elastic.Transport.Tests.csproj +++ b/tests/Elastic.Transport.Tests/Elastic.Transport.Tests.csproj @@ -4,6 +4,7 @@ net8.0;net481 True false + CS8002 @@ -20,6 +21,7 @@ + diff --git a/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs b/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs index aacd376..79562d1 100644 --- a/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs +++ b/tests/Elastic.Transport.Tests/ResponseBuilderDisposeTests.cs @@ -17,8 +17,8 @@ namespace Elastic.Transport.Tests; public class ResponseBuilderDisposeTests { - private readonly ITransportConfiguration _settings = InMemoryConnectionFactory.Create().DisableDirectStreaming(false); - private readonly ITransportConfiguration _settingsDisableDirectStream = InMemoryConnectionFactory.Create().DisableDirectStreaming(); + private readonly ITransportConfiguration _settings = InMemoryConnectionFactory.Create() with { DisableDirectStreaming = false }; + private readonly ITransportConfiguration _settingsDisableDirectStream = InMemoryConnectionFactory.Create() with { DisableDirectStreaming = true }; [Fact] public async Task StreamResponseWithPotentialBody_StreamIsNotDisposed() => @@ -65,20 +65,23 @@ private async Task AssertResponse(bool disableDirectStreaming, int statusCode { ITransportConfiguration config; - if (skipStatusCode > -1 ) - config = InMemoryConnectionFactory.Create(productRegistration) - .DisableDirectStreaming(disableDirectStreaming) - .SkipDeserializationForStatusCodes(skipStatusCode); + var memoryStreamFactory = new TrackMemoryStreamFactory(); + + if (skipStatusCode > -1) + config = InMemoryConnectionFactory.Create(productRegistration) with + { + DisableDirectStreaming = disableDirectStreaming, SkipDeserializationForStatusCodes = [skipStatusCode] + }; + else if (productRegistration is not null) - config = InMemoryConnectionFactory.Create(productRegistration) - .DisableDirectStreaming(disableDirectStreaming); + config = InMemoryConnectionFactory.Create(productRegistration) with { DisableDirectStreaming = disableDirectStreaming, }; else config = disableDirectStreaming ? _settingsDisableDirectStream : _settings; - var memoryStreamFactory = new TrackMemoryStreamFactory(); + config = new TransportConfiguration(config) { MemoryStreamFactory = memoryStreamFactory }; var endpoint = new Endpoint(new EndpointPath(httpMethod, "/"), new Node(new Uri("http://localhost:9200"))); - var requestData = new RequestData(config, null, customResponseBuilder, memoryStreamFactory); + var requestData = new RequestData(config, null, customResponseBuilder); var stream = new TrackDisposeStream(); diff --git a/tests/Elastic.Transport.Tests/Test.cs b/tests/Elastic.Transport.Tests/Test.cs deleted file mode 100644 index 8f096d7..0000000 --- a/tests/Elastic.Transport.Tests/Test.cs +++ /dev/null @@ -1,85 +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 Elastic.Transport.Products; -using Elastic.Transport.Products.Elasticsearch; - -// ReSharper disable UnusedVariable -// ReSharper disable NotAccessedField.Local - -namespace Elastic.Transport.Tests -{ - public class Test - { - public void Usage() - { - var pool = new StaticNodePool(new[] {new Node(new Uri("http://localhost:9200"))}); - var requestInvoker = new HttpRequestInvoker(); - var serializer = LowLevelRequestResponseSerializer.Instance; - var product = ElasticsearchProductRegistration.Default; - - var settings = new TransportConfiguration(pool, requestInvoker, serializer, product); - var transport = new DistributedTransport(settings); - - var response = transport.Request(HttpMethod.GET, "/"); - } - - public void MinimalUsage() - { - var settings = new TransportConfiguration(new Uri("http://localhost:9200")); - var transport = new DistributedTransport(settings); - - var response = transport.Get("/"); - - var headResponse = transport.Head("/"); - } - - public void MinimalElasticsearch() - { - var uri = new Uri("http://localhost:9200"); - var settings = new TransportConfiguration(uri, ElasticsearchProductRegistration.Default); - var transport = new DistributedTransport(settings); - - var response = transport.Get("/"); - - var headResponse = transport.Head("/"); - } - - public void MinimalUsageWithRequestParameters() - { - var settings = new TransportConfiguration(new Uri("http://localhost:9200")); - var transport = new DistributedTransport(settings); - - var response = transport.Get("/", new DefaultRequestParameters()); - - var headResponse = transport.Head("/"); - } - - public class MyClientConfiguration : TransportConfigurationBase - { - public MyClientConfiguration( - NodePool nodePool = null, - IRequestInvoker transportCLient = null, - Serializer requestResponseSerializer = null, - ProductRegistration productRegistration = null) - : base( - nodePool ?? new SingleNodePool(new Uri("http://default-endpoint.example")) - , transportCLient, requestResponseSerializer, productRegistration) - { - } - - private string _setting; - public MyClientConfiguration NewSettings(string value) => Assign(value, (c, v) => _setting = v); - } - - public void ExtendingConfiguration() - { - var clientConfiguration = new MyClientConfiguration() - .NewSettings("some-value"); - - var transport = new DistributedTransport(clientConfiguration); - } - } -} diff --git a/tests/Elastic.Transport.Tests/UsageTests.cs b/tests/Elastic.Transport.Tests/UsageTests.cs new file mode 100644 index 0000000..fab3719 --- /dev/null +++ b/tests/Elastic.Transport.Tests/UsageTests.cs @@ -0,0 +1,84 @@ +// 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.Products; +using Elastic.Transport.Products.Elasticsearch; + +// ReSharper disable UnusedVariable +// ReSharper disable NotAccessedField.Local + +namespace Elastic.Transport.Tests; + +public class UsageTests +{ + public void Usage() + { + var pool = new StaticNodePool(new[] {new Node(new Uri("http://localhost:9200"))}); + var requestInvoker = new HttpRequestInvoker(); + var serializer = LowLevelRequestResponseSerializer.Instance; + var product = ElasticsearchProductRegistration.Default; + + var settings = new TransportConfiguration(pool, requestInvoker, serializer, product); + var transport = new DistributedTransport(settings); + + var response = transport.Request(HttpMethod.GET, "/"); + } + + public void MinimalUsage() + { + var settings = new TransportConfiguration(new Uri("http://localhost:9200")); + var transport = new DistributedTransport(settings); + + var response = transport.Get("/"); + + var headResponse = transport.Head("/"); + } + + public void MinimalElasticsearch() + { + var uri = new Uri("http://localhost:9200"); + var settings = new TransportConfiguration(uri, ElasticsearchProductRegistration.Default); + var transport = new DistributedTransport(settings); + + var response = transport.Get("/"); + + var headResponse = transport.Head("/"); + } + + public void MinimalUsageWithRequestParameters() + { + var settings = new TransportConfiguration(new Uri("http://localhost:9200")); + var transport = new DistributedTransport(settings); + + var response = transport.Get("/", new DefaultRequestParameters()); + + var headResponse = transport.Head("/"); + } + + public class MyClientConfiguration : TransportConfigurationDescriptorBase + { + public MyClientConfiguration( + NodePool nodePool = null, + IRequestInvoker transportCLient = null, + Serializer requestResponseSerializer = null, + ProductRegistration productRegistration = null) + : base( + nodePool ?? new SingleNodePool(new Uri("http://default-endpoint.example")) + , transportCLient, requestResponseSerializer, productRegistration) + { + } + + private string _setting; + public MyClientConfiguration NewSettings(string value) => Assign(value, (c, v) => _setting = v); + } + + public void ExtendingConfiguration() + { + var clientConfiguration = new MyClientConfiguration() + .NewSettings("some-value"); + + var transport = new DistributedTransport(clientConfiguration); + } +}