diff --git a/src/LaunchDarkly.EventSource/Configuration.cs b/src/LaunchDarkly.EventSource/Configuration.cs index 3be64c2..6ec42db 100644 --- a/src/LaunchDarkly.EventSource/Configuration.cs +++ b/src/LaunchDarkly.EventSource/Configuration.cs @@ -7,18 +7,19 @@ namespace LaunchDarkly.EventSource { /// - /// A class used + /// An immutable class containing configuration properties for . /// + /// public sealed class Configuration - { + { #region Types - - public delegate HttpContent HttpContentFactory(); - + + public delegate HttpContent HttpContentFactory(); + #endregion #region Constants - + public static readonly TimeSpan DefaultDelayRetryDuration = TimeSpan.FromMilliseconds(1000); public static readonly TimeSpan DefaultConnectionTimeout = TimeSpan.FromMilliseconds(10000); public static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); @@ -50,8 +51,8 @@ public sealed class Configuration /// public TimeSpan DelayRetryDuration { get; } - /// - /// The amount of time a connection must stay open before the EventSource resets its backoff delay. + /// + /// The amount of time a connection must stay open before the EventSource resets its backoff delay. /// public TimeSpan BackoffResetThreshold { get; } @@ -81,17 +82,22 @@ public sealed class Configuration /// /// The HttpMessageHandler that will be used for the HTTP client, or null for the default handler. /// - public HttpMessageHandler MessageHandler { get; } - - /// - /// The HTTP method that will be used when connecting to the EventSource API. + public HttpMessageHandler MessageHandler { get; } + + /// + /// The HttpClient that will be used as the HTTP client, or null for a new HttpClient. + /// + public HttpClient HttpClient { get; } + + /// + /// The HTTP method that will be used when connecting to the EventSource API. /// public HttpMethod Method { get; } - /// - /// A factory for HTTP request body content, if the HTTP method is one that allows a request body. - /// is one that allows a request body. This is in the form of a factory function because the request - /// may need to be sent more than once. + /// + /// A factory for HTTP request body content, if the HTTP method is one that allows a request body. + /// is one that allows a request body. This is in the form of a factory function because the request + /// may need to be sent more than once. /// public HttpContentFactory RequestBodyFactory { get; } @@ -120,7 +126,8 @@ public TimeSpan MaximumDelayRetryDuration /// /// The URI used to connect to the remote EventSource API. /// The message handler to use when sending API requests. If null, the is used. - /// The connection timeout. If null, defaults to 10 seconds. + /// The http client to be used when sending API requests. You can specify either or httpClient. + /// The connection timeout. If null, defaults to 10 seconds. Can not be used with /// The time to wait before attempting to reconnect to the EventSource API. If null, defaults to 1 second. /// The timeout when reading data from the EventSource API. If null, defaults to 5 minutes. /// Request headers used when connecting to the remote EventSource API. @@ -129,8 +136,9 @@ public TimeSpan MaximumDelayRetryDuration /// The HTTP method used to connect to the remote EventSource API. /// A function that produces an HTTP request body to send to the remote EventSource API. /// Throws ArgumentNullException if the uri parameter is null. + /// Throws ArgumentException if both httpClient and messageHandler is not null. /// - ///

is less than zero.

+ ///

is less than zero.

///

- or -

///

is greater than 30 seconds.

///

- or -

@@ -138,28 +146,37 @@ public TimeSpan MaximumDelayRetryDuration ///
public Configuration(Uri uri, HttpMessageHandler messageHandler = null, TimeSpan? connectionTimeout = null, TimeSpan? delayRetryDuration = null, TimeSpan? readTimeout = null, IDictionary requestHeaders = null, string lastEventId = null, ILog logger = null, - HttpMethod method = null, HttpContentFactory requestBodyFactory = null, TimeSpan? backoffResetThreshold = null) + HttpMethod method = null, HttpContentFactory requestBodyFactory = null, TimeSpan? backoffResetThreshold = null, HttpClient httpClient = null) { - if (uri == null) + if (uri == null) { - throw new ArgumentNullException(nameof(uri)); + throw new ArgumentNullException(nameof(uri)); } - if (connectionTimeout.HasValue) + if (connectionTimeout.HasValue) { - CheckConnectionTimeout(connectionTimeout.Value); + CheckConnectionTimeout(connectionTimeout.Value); } - if (delayRetryDuration.HasValue) - { - CheckDelayRetryDuration(delayRetryDuration.Value); + if (delayRetryDuration.HasValue) + { + CheckDelayRetryDuration(delayRetryDuration.Value); } - if (readTimeout.HasValue) - { - CheckReadTimeout(readTimeout.Value); + if (readTimeout.HasValue) + { + CheckReadTimeout(readTimeout.Value); + } + if (httpClient != null && messageHandler != null) + { + throw new ArgumentException(Resources.Configuration_HttpClient_With_MessageHandler, nameof(messageHandler)); + } + if (httpClient != null && connectionTimeout != null) + { + throw new ArgumentException(Resources.Configuration_HttpClient_With_ConnectionTimeout, nameof(connectionTimeout)); } Uri = uri; MessageHandler = messageHandler; + HttpClient = httpClient; ConnectionTimeout = connectionTimeout ?? DefaultConnectionTimeout; DelayRetryDuration = delayRetryDuration ?? DefaultDelayRetryDuration; BackoffResetThreshold = backoffResetThreshold ?? DefaultBackoffResetThreshold; @@ -169,54 +186,54 @@ public Configuration(Uri uri, HttpMessageHandler messageHandler = null, TimeSpan Logger = logger; Method = method; RequestBodyFactory = requestBodyFactory; - } - + } + #endregion - + #region Public Methods - - /// - /// Provides a new for constructing a configuration. - /// - /// the EventSource URI - /// a new builder instance - public static ConfigurationBuilder Builder(Uri uri) - { - return new ConfigurationBuilder(uri); - } - + + /// + /// Provides a new for constructing a configuration. + /// + /// the EventSource URI + /// a new builder instance + public static ConfigurationBuilder Builder(Uri uri) + { + return new ConfigurationBuilder(uri); + } + #endregion - + #region Internal Methods - - internal static void CheckConnectionTimeout(TimeSpan connectionTimeout) - { - if (connectionTimeout != Timeout.InfiniteTimeSpan && connectionTimeout < TimeSpan.Zero) + + internal static void CheckConnectionTimeout(TimeSpan connectionTimeout) + { + if (connectionTimeout != Timeout.InfiniteTimeSpan && connectionTimeout < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(connectionTimeout), Resources.Configuration_Value_Greater_Than_Zero); + } + } + + internal static void CheckDelayRetryDuration(TimeSpan delayRetryDuration) + { + if (delayRetryDuration > MaximumRetryDuration) { - throw new ArgumentOutOfRangeException(nameof(connectionTimeout), Resources.Configuration_Value_Greater_Than_Zero); - } - } - - internal static void CheckDelayRetryDuration(TimeSpan delayRetryDuration) - { - if (delayRetryDuration > MaximumRetryDuration) + throw new ArgumentOutOfRangeException(nameof(delayRetryDuration), string.Format(Resources.Configuration_RetryDuration_Exceeded, MaximumRetryDuration.Milliseconds)); + } + if (delayRetryDuration < TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(delayRetryDuration), string.Format(Resources.Configuration_RetryDuration_Exceeded, MaximumRetryDuration.Milliseconds)); - } - if (delayRetryDuration < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(delayRetryDuration), Resources.Configuration_Value_Greater_Than_Zero); - } - } - - internal static void CheckReadTimeout(TimeSpan readTimeout) - { - if (readTimeout < TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(delayRetryDuration), Resources.Configuration_Value_Greater_Than_Zero); + } + } + + internal static void CheckReadTimeout(TimeSpan readTimeout) + { + if (readTimeout < TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(readTimeout), Resources.Configuration_Value_Greater_Than_Zero); - } - } - + throw new ArgumentOutOfRangeException(nameof(readTimeout), Resources.Configuration_Value_Greater_Than_Zero); + } + } + #endregion } } diff --git a/src/LaunchDarkly.EventSource/ConfigurationBuilder.cs b/src/LaunchDarkly.EventSource/ConfigurationBuilder.cs index 60ca4cd..681428c 100644 --- a/src/LaunchDarkly.EventSource/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.EventSource/ConfigurationBuilder.cs @@ -1,135 +1,141 @@ using Common.Logging; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; - -namespace LaunchDarkly.EventSource -{ - /// - /// A standard Builder pattern for constructing a instance. - /// - /// Initialize a builder by calling new ConfigurationBuilder(uri) or - /// Configuration.Builder(uri). The URI is always required; all other properties - /// are set to defaults. Use the builder's setter methods to modify any desired properties; - /// setter methods can be chained. Then call Build() to construct the final immutable - /// Configuration. - /// - /// All setter methods will throw ArgumentException if called with an invalid value, - /// so it is never possible for Build() to fail. - /// - public class ConfigurationBuilder - { - #region Private Fields - +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; + +namespace LaunchDarkly.EventSource +{ + /// + /// A standard Builder pattern for constructing a instance. + /// + /// Initialize a builder by calling new ConfigurationBuilder(uri) or + /// Configuration.Builder(uri). The URI is always required; all other properties + /// are set to defaults. Use the builder's setter methods to modify any desired properties; + /// setter methods can be chained. Then call Build() to construct the final immutable + /// Configuration. + /// + /// All setter methods will throw ArgumentException if called with an invalid value, + /// so it is never possible for Build() to fail. + /// + public class ConfigurationBuilder + { + #region Private Fields + private readonly Uri _uri; - private TimeSpan _connectionTimeout = Configuration.DefaultConnectionTimeout; + private TimeSpan? _connectionTimeout = null; private TimeSpan _delayRetryDuration = Configuration.DefaultDelayRetryDuration; private TimeSpan _backoffResetThreshold = Configuration.DefaultBackoffResetThreshold; private TimeSpan _readTimeout = Configuration.DefaultReadTimeout; private string _lastEventId; private ILog _logger; private IDictionary _requestHeaders = new Dictionary(); - private HttpMessageHandler _messageHandler; + private HttpMessageHandler _messageHandler; + private HttpClient _httpClient; private HttpMethod _method = HttpMethod.Get; - private Configuration.HttpContentFactory _requestBodyFactory; - - #endregion - - #region Constructor - - public ConfigurationBuilder(Uri uri) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - this._uri = uri; - } - - #endregion - - #region Public Methods - - /// - /// Constructs a instance based on the current builder properies. - /// - /// the configuration - public Configuration Build() - { - return new Configuration(_uri, _messageHandler, _connectionTimeout, _delayRetryDuration, _readTimeout, - _requestHeaders, _lastEventId, _logger, _method, _requestBodyFactory); - } - - /// - /// Sets the connection timeout value used when connecting to the EventSource API. - /// - /// - /// The default value is . - /// - /// the timeout - /// the builder - public ConfigurationBuilder ConnectionTimeout(TimeSpan connectionTimeout) - { - Configuration.CheckConnectionTimeout(connectionTimeout); - _connectionTimeout = connectionTimeout; - return this; - } - - /// - /// Sets the initial amount of time to wait before attempting to reconnect to the EventSource API. - /// - /// - /// If the connection fails more than once, the retry delay will increase from this value using - /// a backoff algorithm. - /// - /// The default value is . The maximum - /// allowed value is . - /// - /// the initial retry delay - /// the builder - public ConfigurationBuilder DelayRetryDuration(TimeSpan delayRetryDuration) - { - Configuration.CheckDelayRetryDuration(delayRetryDuration); - _delayRetryDuration = delayRetryDuration; - return this; - } - - /// - /// Sets the amount of time a connection must stay open before the EventSource resets its backoff delay. - /// - /// - /// If a connection fails before the threshold has elapsed, the delay before reconnecting will be greater - /// than the last delay; if it fails after the threshold, the delay will start over at the initial minimum - /// value. This prevents long delays from occurring on connections that are only rarely restarted. - /// - /// The default value is . - /// - /// the threshold time - /// the builder - public ConfigurationBuilder BackoffResetThreshold(TimeSpan backoffResetThreshold) - { - _backoffResetThreshold = backoffResetThreshold; - return this; - } - + private Configuration.HttpContentFactory _requestBodyFactory; + + #endregion + + #region Constructor + + public ConfigurationBuilder(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + this._uri = uri; + } + + #endregion + + #region Public Methods + + /// + /// Constructs a instance based on the current builder properies. + /// + /// the configuration + public Configuration Build() + { + return new Configuration(_uri, _messageHandler, _connectionTimeout, _delayRetryDuration, _readTimeout, + _requestHeaders, _lastEventId, _logger, _method, _requestBodyFactory, httpClient:_httpClient); + } + + /// + /// Sets the connection timeout value used when connecting to the EventSource API. + /// + /// + /// + /// The default value is . + /// + /// + /// Do not use this property if you are using to specify a custom HTTP client. Doing so will cause to throw an exception. + /// + /// + /// the timeout + /// the builder + public ConfigurationBuilder ConnectionTimeout(TimeSpan connectionTimeout) + { + Configuration.CheckConnectionTimeout(connectionTimeout); + _connectionTimeout = connectionTimeout; + return this; + } + + /// + /// Sets the initial amount of time to wait before attempting to reconnect to the EventSource API. + /// + /// + /// If the connection fails more than once, the retry delay will increase from this value using + /// a backoff algorithm. + /// + /// The default value is . The maximum + /// allowed value is . + /// + /// the initial retry delay + /// the builder + public ConfigurationBuilder DelayRetryDuration(TimeSpan delayRetryDuration) + { + Configuration.CheckDelayRetryDuration(delayRetryDuration); + _delayRetryDuration = delayRetryDuration; + return this; + } + + /// + /// Sets the amount of time a connection must stay open before the EventSource resets its backoff delay. + /// + /// + /// If a connection fails before the threshold has elapsed, the delay before reconnecting will be greater + /// than the last delay; if it fails after the threshold, the delay will start over at the initial minimum + /// value. This prevents long delays from occurring on connections that are only rarely restarted. + /// + /// The default value is . + /// + /// the threshold time + /// the builder + public ConfigurationBuilder BackoffResetThreshold(TimeSpan backoffResetThreshold) + { + _backoffResetThreshold = backoffResetThreshold; + return this; + } + /// /// Sets the timeout when reading from the EventSource API. - /// - /// - /// The connection will be automatically dropped and restarted if the server sends no data within - /// this interval. This prevents keeping a stale connection that may no longer be working. It is common - /// for SSE servers to send a simple comment line (":") as a heartbeat to prevent timeouts. - /// - /// The default value is . - /// - public ConfigurationBuilder ReadTimeout(TimeSpan readTimeout) - { - Configuration.CheckReadTimeout(readTimeout); - _readTimeout = readTimeout; - return this; - } - + ///
+ /// + /// The connection will be automatically dropped and restarted if the server sends no data within + /// this interval. This prevents keeping a stale connection that may no longer be working. It is common + /// for SSE servers to send a simple comment line (":") as a heartbeat to prevent timeouts. + /// + /// The default value is . + /// + public ConfigurationBuilder ReadTimeout(TimeSpan readTimeout) + { + Configuration.CheckReadTimeout(readTimeout); + _readTimeout = readTimeout; + return this; + } + /// /// Sets the last event identifier. /// @@ -138,101 +144,124 @@ public ConfigurationBuilder ReadTimeout(TimeSpan readTimeout) /// This normally corresponds to the field of a previously /// received event. /// - /// the event identifier - /// the builder - public ConfigurationBuilder LastEventId(string lastEventId) - { - _lastEventId = lastEventId; - return this; - } - - /// - /// Sets a custom logger to be used for all EventSource log output. - /// - /// - /// By default, EventSource will call to creates its - /// own logger. - /// - /// a logger instance - /// the builder - public ConfigurationBuilder Logger(ILog logger) - { - _logger = logger; - return this; - } - + /// the event identifier + /// the builder + public ConfigurationBuilder LastEventId(string lastEventId) + { + _lastEventId = lastEventId; + return this; + } + + /// + /// Sets a custom logger to be used for all EventSource log output. + /// + /// + /// By default, EventSource will call to creates its + /// own logger. + /// + /// a logger instance + /// the builder + public ConfigurationBuilder Logger(ILog logger) + { + _logger = logger; + return this; + } + /// /// Sets the request headers to be sent with each EventSource HTTP request. /// /// the headers (must not be null) /// the builder - public ConfigurationBuilder RequestHeaders(IDictionary headers) - { - _requestHeaders = headers ?? throw new ArgumentNullException(nameof(headers)); - return this; - } - - /// - /// Adds a request header to be sent with each EventSource HTTP request. - /// - /// the header name - /// the header value - /// the builder - public ConfigurationBuilder RequestHeader(string name, string value) - { - _requestHeaders[name] = value; - return this; - } - + public ConfigurationBuilder RequestHeaders(IDictionary headers) + { + _requestHeaders = headers ?? throw new ArgumentNullException(nameof(headers)); + return this; + } + + /// + /// Adds a request header to be sent with each EventSource HTTP request. + /// + /// the header name + /// the header value + /// the builder + public ConfigurationBuilder RequestHeader(string name, string value) + { + _requestHeaders[name] = value; + return this; + } + /// /// Sets the HttpMessageHandler that will be used for the HTTP client, or null for the default handler. /// - /// the message handler implementation - /// the builder - public ConfigurationBuilder MessageHandler(HttpMessageHandler handler) - { - this._messageHandler = handler; - return this; - } - - /// - /// Sets the HTTP method that will be used when connecting to the EventSource API. + /// + /// Do not use this property if you are using to specify a custom HTTP client. Doing so will cause to throw an exception. + /// + /// the message handler implementation + /// the builder + public ConfigurationBuilder MessageHandler(HttpMessageHandler handler) + { + this._messageHandler = handler; + return this; + } + + /// + /// Specifies that EventSource should use a specific HttpClient instance for HTTP requests. + /// + /// + /// + /// Normally, EventSource creates its own HttpClient and disposes of it when you dispose of the EventSource. If you provide your own HttpClient using this method, you are responsible for managing the HttpClient's lifecycle-- EventSource will not dispose of it. + /// + /// + /// EventSource will not modify this client's properties, so you should not the ConfigurationBuilder methods and if you use this option. Doing so will cause to throw an exception. + /// + /// + /// an HttpClient instance, or null to use the default behavior + /// the builder + public ConfigurationBuilder HttpClient(HttpClient client) + { + this._httpClient = client; + return this; + } + + /// + /// Sets the HTTP method that will be used when connecting to the EventSource API. /// /// /// By default, this is . /// - public ConfigurationBuilder Method(HttpMethod method) - { - this._method = method ?? throw new ArgumentNullException(nameof(method)); - return this; - } - - /// - /// Sets a factory for HTTP request body content, if the HTTP method is one that allows a request body. - /// - /// - /// This is in the form of a factory function because the request may need to be sent more than once. - /// - /// the factory function, or null for none - /// the builder - public ConfigurationBuilder RequestBodyFactory(Configuration.HttpContentFactory factory) - { - this._requestBodyFactory = factory; - return this; - } - - /// - /// Equivalent , but for content - /// that is a simple string. - /// - /// the content - /// the Content-Type header - /// the builder - public ConfigurationBuilder RequestBody(string bodyString, string contentType) - { - return RequestBodyFactory(() => new StringContent(bodyString, Encoding.UTF8, contentType)); - } - - #endregion - } -} + public ConfigurationBuilder Method(HttpMethod method) + { + this._method = method ?? throw new ArgumentNullException(nameof(method)); + return this; + } + + /// + /// Sets a factory for HTTP request body content, if the HTTP method is one that allows a request body. + /// + /// + /// This is in the form of a factory function because the request may need to be sent more than once. + /// + /// the factory function, or null for none + /// the builder + public ConfigurationBuilder RequestBodyFactory(Configuration.HttpContentFactory factory) + { + this._requestBodyFactory = factory; + return this; + } + + /// + /// Equivalent , but for content + /// that is a simple string. + /// + /// the content + /// the Content-Type header + /// the builder + public ConfigurationBuilder RequestBody(string bodyString, string contentType) + { + return RequestBodyFactory(() => new StringContent(bodyString, Encoding.UTF8, contentType)); + } + + #endregion + + } +} diff --git a/src/LaunchDarkly.EventSource/EventSource.cs b/src/LaunchDarkly.EventSource/EventSource.cs index b3f450d..3a778e4 100644 --- a/src/LaunchDarkly.EventSource/EventSource.cs +++ b/src/LaunchDarkly.EventSource/EventSource.cs @@ -111,7 +111,7 @@ public EventSource(Configuration configuration) _backOff = new ExponentialBackoffWithDecorrelation(_retryDelay, Configuration.MaximumRetryDuration); - _httpClient = CreateHttpClient(); + _httpClient = _configuration.HttpClient ?? CreateHttpClient(); } #endregion @@ -226,7 +226,12 @@ public void Close() Close(ReadyState.Shutdown); } CancelCurrentRequest(); - _httpClient.Dispose(); + + // do not dispose httpClient if it is user provided + if (_configuration.HttpClient == null) + { + _httpClient.Dispose(); + } } /// diff --git a/src/LaunchDarkly.EventSource/Resources.Designer.cs b/src/LaunchDarkly.EventSource/Resources.Designer.cs index 7745d9e..96eb915 100644 --- a/src/LaunchDarkly.EventSource/Resources.Designer.cs +++ b/src/LaunchDarkly.EventSource/Resources.Designer.cs @@ -1,163 +1,181 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace LaunchDarkly.EventSource { - using System; - using System.Reflection; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LaunchDarkly.EventSource.Resources", typeof(Resources).GetTypeInfo().Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The maximum retry duration is {0}.. - /// - internal static string Configuration_RetryDuration_Exceeded { - get { - return ResourceManager.GetString("Configuration_RetryDuration_Exceeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value must be greater than zero.. - /// - internal static string Configuration_Value_Greater_Than_Zero { - get { - return ResourceManager.GetString("Configuration_Value_Greater_Than_Zero", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remote EventSource API returned Http Status Code 204.. - /// - internal static string EventSource_204_Response { - get { - return ResourceManager.GetString("EventSource_204_Response", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid attempt to call Start() while the connection is {0}.. - /// - internal static string EventSource_Already_Started { - get { - return ResourceManager.GetString("EventSource_Already_Started", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Http Status Code '{0}' indicates an unsuccessful response returned from the remote EventSource API.. - /// - internal static string EventSource_HttpResponse_Not_Successful { - get { - return ResourceManager.GetString("EventSource_HttpResponse_Not_Successful", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to HTTP Content-Type returned from the remote EventSource API does not match 'text/event-stream'.. - /// - internal static string EventSource_Invalid_MediaType { - get { - return ResourceManager.GetString("EventSource_Invalid_MediaType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to EventSource.Close called. - /// - internal static string EventSource_Logger_Closed { - get { - return ResourceManager.GetString("EventSource_Logger_Closed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Encountered exception in EventSourceService.ConnectToEventSourceApi method. Exception Message: {0}. - /// - internal static string EventSource_Logger_Connection_Error { - get { - return ResourceManager.GetString("EventSource_Logger_Connection_Error", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to EventSource Disconnected. Automatically delaying {0}ms before reconnecting.. - /// - internal static string EventSource_Logger_Disconnected { - get { - return ResourceManager.GetString("EventSource_Logger_Disconnected", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Http Response contained no content.. - /// - internal static string EventSource_Response_Content_Empty { - get { - return ResourceManager.GetString("EventSource_Response_Content_Empty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A timeout occurred while reading the request from the remote EventSource API. - /// - internal static string EventSourceService_Read_Timeout { - get { - return ResourceManager.GetString("EventSourceService_Read_Timeout", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace LaunchDarkly.EventSource { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LaunchDarkly.EventSource.Resources", typeof(Resources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to HttpClient can not be used with ConnectionTimeout. Use Timeout property of httpClient instead.. + /// + internal static string Configuration_HttpClient_With_ConnectionTimeout { + get { + return ResourceManager.GetString("Configuration_HttpClient_With_ConnectionTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HttpClient can not be used with MessageHandler.. + /// + internal static string Configuration_HttpClient_With_MessageHandler { + get { + return ResourceManager.GetString("Configuration_HttpClient_With_MessageHandler", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum retry duration is {0}.. + /// + internal static string Configuration_RetryDuration_Exceeded { + get { + return ResourceManager.GetString("Configuration_RetryDuration_Exceeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be greater than zero.. + /// + internal static string Configuration_Value_Greater_Than_Zero { + get { + return ResourceManager.GetString("Configuration_Value_Greater_Than_Zero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remote EventSource API returned Http Status Code 204.. + /// + internal static string EventSource_204_Response { + get { + return ResourceManager.GetString("EventSource_204_Response", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid attempt to call Start() while the connection is {0}.. + /// + internal static string EventSource_Already_Started { + get { + return ResourceManager.GetString("EventSource_Already_Started", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Http Status Code '{0}' indicates an unsuccessful response returned from the remote EventSource API.. + /// + internal static string EventSource_HttpResponse_Not_Successful { + get { + return ResourceManager.GetString("EventSource_HttpResponse_Not_Successful", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HTTP Content-Type returned from the remote EventSource API does not match 'text/event-stream'.. + /// + internal static string EventSource_Invalid_MediaType { + get { + return ResourceManager.GetString("EventSource_Invalid_MediaType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EventSource.Close called. + /// + internal static string EventSource_Logger_Closed { + get { + return ResourceManager.GetString("EventSource_Logger_Closed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Encountered exception in EventSourceService.ConnectToEventSourceApi method. Exception Message: {0}. + /// + internal static string EventSource_Logger_Connection_Error { + get { + return ResourceManager.GetString("EventSource_Logger_Connection_Error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EventSource Disconnected. Automatically delaying {0}ms before reconnecting.. + /// + internal static string EventSource_Logger_Disconnected { + get { + return ResourceManager.GetString("EventSource_Logger_Disconnected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Http Response contained no content.. + /// + internal static string EventSource_Response_Content_Empty { + get { + return ResourceManager.GetString("EventSource_Response_Content_Empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A timeout occurred while reading the request from the remote EventSource API. + /// + internal static string EventSourceService_Read_Timeout { + get { + return ResourceManager.GetString("EventSourceService_Read_Timeout", resourceCulture); + } + } + } +} diff --git a/src/LaunchDarkly.EventSource/Resources.resx b/src/LaunchDarkly.EventSource/Resources.resx index 1f89e26..1ba0f72 100644 --- a/src/LaunchDarkly.EventSource/Resources.resx +++ b/src/LaunchDarkly.EventSource/Resources.resx @@ -117,6 +117,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ConfigurationBuilder.HttpClient cannot be used with ConfigurationBuilder.MessageHandler. + + + ConfigurationBuilder.HttpClient cannot be used with ConfigurationBuilder.ConnectionTimeout. Use Timeout property of HttpClient instead. + The maximum retry duration is {0}. diff --git a/test/LaunchDarkly.EventSource.Tests/ConfigurationBuilderTests.cs b/test/LaunchDarkly.EventSource.Tests/ConfigurationBuilderTests.cs index d9112ca..dfcd935 100644 --- a/test/LaunchDarkly.EventSource.Tests/ConfigurationBuilderTests.cs +++ b/test/LaunchDarkly.EventSource.Tests/ConfigurationBuilderTests.cs @@ -192,6 +192,20 @@ public void BuilderSetsMessageHandler() Assert.Same(h, b.Build().MessageHandler); } + [Fact] + public void HttpClientDefaultsToNull() + { + Assert.Null(Configuration.Builder(uri).Build().HttpClient); + } + + [Fact] + public void BuilderSetsHttpClient() + { + var h = new HttpClient(); + var b = Configuration.Builder(uri).HttpClient(h); + Assert.Same(h, b.Build().HttpClient); + } + [Fact] public void MethodDefaultsToGet() { diff --git a/test/LaunchDarkly.EventSource.Tests/ConfigurationTests.cs b/test/LaunchDarkly.EventSource.Tests/ConfigurationTests.cs index 8d97372..c029d1a 100644 --- a/test/LaunchDarkly.EventSource.Tests/ConfigurationTests.cs +++ b/test/LaunchDarkly.EventSource.Tests/ConfigurationTests.cs @@ -1,4 +1,6 @@ using System; +using System.Net.Http; + using Xunit; @@ -17,6 +19,30 @@ public void Configuration_constructor_throws_exception_when_uri_is_null() Assert.IsType(e); } + [Fact] + public void Configuration_constructor_throws_exception_when_http_client_and_messageHandler_is_provided() + { + var stubMessageHandler = new StubMessageHandler(); + var e = Record.Exception(() => + new Configuration(uri: _uri, + httpClient: new HttpClient(stubMessageHandler), + messageHandler: stubMessageHandler)); + + Assert.IsType(e); + } + + [Fact] + public void Configuration_constructor_throws_exception_when_http_client_and_connectionTimeout_is_provided() + { + var stubMessageHandler = new StubMessageHandler(); + var e = Record.Exception(() => + new Configuration(uri: _uri, + httpClient: new HttpClient(stubMessageHandler), + connectionTimeout: TimeSpan.Zero)); + + Assert.IsType(e); + } + [Fact] public void Configuration_constructor_throws_exception_when_connection_timeout_is_negative() { diff --git a/test/LaunchDarkly.EventSource.Tests/EventSourceTests.cs b/test/LaunchDarkly.EventSource.Tests/EventSourceTests.cs index ed7d9bc..ff0ee27 100644 --- a/test/LaunchDarkly.EventSource.Tests/EventSourceTests.cs +++ b/test/LaunchDarkly.EventSource.Tests/EventSourceTests.cs @@ -72,10 +72,10 @@ public async Task When_an_event_message_SSE_is_received_then_a_message_event_is_ var handler = new StubMessageHandler(); handler.QueueResponse(StubResponse.StartStream(StreamAction.Write(sse))); - var evt = new EventSource(new Configuration(_uri, handler)); - + var evt = new EventSource(new Configuration(_uri, handler)); + var m = new MessageReceiver(); - evt.MessageReceived += m; + evt.MessageReceived += m; evt.MessageReceived += ((_, e) => evt.Close()); await evt.StartAsync(); @@ -83,6 +83,41 @@ public async Task When_an_event_message_SSE_is_received_then_a_message_event_is_ Assert.Equal("put", m.RequireSingleEvent().EventName); } + [Fact] + public async Task When_an_event_message_SSE_is_received_with_http_client_then_a_message_event_is_raised() + { + var sse = "event: httpclient\ndata: this is a test message with httpclient\n\n"; + + var handler = new StubMessageHandler(); + handler.QueueResponse(StubResponse.StartStream(StreamAction.Write(sse))); + + var client = new HttpClient(handler); + var evt = new EventSource(new Configuration(_uri, httpClient: client)); + + var m = new MessageReceiver(); + evt.MessageReceived += m; + evt.MessageReceived += ((_, e) => evt.Close()); + + await evt.StartAsync(); + + Assert.Equal("httpclient", m.RequireSingleEvent().EventName); + client.Dispose(); + } + + [Fact] + public async Task When_event_source_closes_do_not_dispose_configured_http_client() + { + var handler = new StubMessageHandler(); + handler.QueueResponse(StubResponse.WithResponse(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Hello")})); + + var client = new HttpClient(handler); + var evt = new EventSource(new Configuration(_uri, httpClient: client)); + evt.Close(); + + await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, _uri)); + client.Dispose(); + } + [Fact] public async Task When_an_message_SSE_contains_id_is_received_then_last_event_id_is_set() { @@ -141,8 +176,8 @@ public async Task HTTP_request_body_can_be_specified() HttpContent content = new StringContent("{}"); Configuration.HttpContentFactory contentFn = () => - { - return content; + { + return content; }; var config = new ConfigurationBuilder(_uri).MessageHandler(handler)