From 060009d56193ed12d6d3efa7a2805c8b39770255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Tue, 21 Jan 2025 16:11:22 -0300 Subject: [PATCH] WIP --- Flagsmith.FlagsmithClient/FlagsmithClient.cs | 247 +++++++++--------- .../FlagsmithConfiguration.cs | 76 +++++- .../IFlagsmithConfiguration.cs | 20 +- 3 files changed, 207 insertions(+), 136 deletions(-) diff --git a/Flagsmith.FlagsmithClient/FlagsmithClient.cs b/Flagsmith.FlagsmithClient/FlagsmithClient.cs index e9b7b6f..baf3d3d 100644 --- a/Flagsmith.FlagsmithClient/FlagsmithClient.cs +++ b/Flagsmith.FlagsmithClient/FlagsmithClient.cs @@ -4,7 +4,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Net.Http; using System.Text; using System.Threading; @@ -37,35 +36,75 @@ namespace Flagsmith /// public class FlagsmithClient : IFlagsmithClient { - private string? ApiUrl { get; set; } - private string? EnvironmentKey { get; set; } - private bool EnableClientSideEvaluation { get; set; } - private int EnvironmentRefreshIntervalSeconds { get; set; } - private Func? DefaultFlagHandler { get; set; } - private ILogger? Logger { get; set; } - private bool EnableAnalytics { get; set; } - private double? RequestTimeout { get; set; } - private int Retries { get; set; } - private CacheConfig CacheConfig { get; set; } - private Dictionary? CustomHeaders { get; set; } private EnvironmentModel? Environment { get; set; } private Dictionary? IdentitiesWithOverridesByIdentifier { get; set; } - private bool OfflineMode { get; set; } - const string DefaultApiUrl = "https://edge.api.flagsmith.com/api/v1/"; - private readonly HttpClient _httpClient; - private readonly PollingManager? _pollingManager; - private readonly IEngine _engine; - private readonly AnalyticsProcessor? _analyticsProcessor; - private readonly RegularFlagListCache? _regularFlagListCache; private readonly ConcurrentDictionary? _flagListCacheDictionary; - private readonly BaseOfflineHandler? _offlineHandler; + private readonly FlagsmithConfiguration _config; + private readonly IEngine _engine = new Engine(); + private PollingManager? _pollingManager; + private AnalyticsProcessor? _analyticsProcessor; + private RegularFlagListCache? _regularFlagListCache; + private ConcurrentDictionary? _flagListCacheDictionary; + + private void Initialise() + { + if (_config.OfflineMode && _config.OfflineHandler is null) + { + throw new Exception("ValueError: offlineHandler must be provided to use offline mode."); + } + else if (_config.DefaultFlagHandler != null && _config.OfflineHandler != null) + { + throw new Exception("ValueError: Cannot use both defaultFlagHandler and offlineHandler."); + } + + if (_config.OfflineHandler != null) + { + Environment = _config.OfflineHandler.GetEnvironment(); + } + + if (!_config.OfflineMode) + { + if (string.IsNullOrEmpty(_config.EnvironmentKey)) + { + throw new Exception("ValueError: environmentKey is required"); + } + if (_config.EnableAnalytics) + _analyticsProcessor = new AnalyticsProcessor(_config.HttpClient, _config.EnvironmentKey, _config.ApiUri.ToString(), _config.Logger, _config.CustomHeaders); + + if (_config.EnableLocalEvaluation) + { + if (!_config.EnvironmentKey!.StartsWith("ser.")) + { + Console.WriteLine( + "In order to use local evaluation, please generate a server key in the environment settings page." + ); + } + + _pollingManager = new PollingManager(GetAndUpdateEnvironmentFromApi, _config.EnvironmentRefreshIntervalSeconds); + Task.Run(async () => await _pollingManager.StartPoll()).GetAwaiter().GetResult(); + } + } + + if (_config.CacheConfig.Enabled) + { + _regularFlagListCache = new RegularFlagListCache(new DateTimeProvider(), + _config.CacheConfig.DurationInMinutes); + _flagListCacheDictionary = new ConcurrentDictionary(); + } + } + + public FlagsmithClient(FlagsmithConfiguration configuration) + { + _config = configuration; + Initialise(); + } /// /// Create flagsmith client. /// /// The environment key obtained from Flagsmith interface. Required unless offlineMode is True /// Override the URL of the Flagsmith API to communicate with. Required unless offlineMode is True - /// Provide logger for logging polling info & errors which is only applicable when client side evalution is enabled and analytics errors. + /// Provide logger for logging polling info and errors which is only applicable when client side evalution is enabled and analytics errors. /// Callable which will be used in the case where flags cannot be retrieved from the API or a non existent feature is requested. /// if enabled, sends additional requests to the Flagsmith API to power flag analytics charts. /// If using local evaluation, specify the interval period between refreshes of local environment data. @@ -83,13 +122,10 @@ public class FlagsmithClient : IFlagsmithClient /// /// A general exception with a error message. Example: Feature not found, etc. /// - /// - /// A general exception with a error message. Example: Feature not found, etc. - /// - + [Obsolete("Use FlagsmithClient(FlagsmithConfiguration) instead.")] public FlagsmithClient( string? environmentKey = null, - string apiUrl = DefaultApiUrl, + string apiUrl = "https://edge.api.flagsmith.com/api/v1/", ILogger? logger = null, Func? defaultFlagHandler = null, bool enableAnalytics = false, @@ -102,91 +138,44 @@ public FlagsmithClient( CacheConfig? cacheConfig = null, bool offlineMode = false, BaseOfflineHandler? offlineHandler = null - ) + ) { - this.EnvironmentKey = environmentKey; - this.Logger = logger; - this.DefaultFlagHandler = defaultFlagHandler; - this.EnableAnalytics = enableAnalytics; - this.EnableClientSideEvaluation = enableClientSideEvaluation; - this.EnvironmentRefreshIntervalSeconds = environmentRefreshIntervalSeconds; - this.CustomHeaders = customHeaders; - this.Retries = retries; - this.RequestTimeout = requestTimeout; - this._httpClient = httpClient ?? new HttpClient(); - this.CacheConfig = cacheConfig ?? new CacheConfig(false); - this.OfflineMode = offlineMode; - this._offlineHandler = offlineHandler; - _engine = new Engine(); - - if (OfflineMode && _offlineHandler is null) + _config = new FlagsmithConfiguration { - throw new Exception("ValueError: offlineHandler must be provided to use offline mode."); - } - else if (DefaultFlagHandler != null && _offlineHandler != null) - { - throw new Exception("ValueError: Cannot use both defaultFlagHandler and offlineHandler."); - } - - if (_offlineHandler != null) - { - Environment = _offlineHandler.GetEnvironment(); - } - - if (!OfflineMode) - { - if (string.IsNullOrEmpty(EnvironmentKey)) - { - throw new Exception("ValueError: environmentKey is required"); - } - - var _apiUrl = apiUrl ?? DefaultApiUrl; - ApiUrl = _apiUrl.EndsWith("/") ? _apiUrl : $"{_apiUrl}/"; - - if (EnableAnalytics) - _analyticsProcessor = new AnalyticsProcessor(this._httpClient, EnvironmentKey, ApiUrl, Logger, CustomHeaders); - - if (EnableClientSideEvaluation) - { - if (!EnvironmentKey!.StartsWith("ser.")) - { - Console.WriteLine( - "In order to use local evaluation, please generate a server key in the environment settings page." - ); - } - - _pollingManager = new PollingManager(GetAndUpdateEnvironmentFromApi, EnvironmentRefreshIntervalSeconds); - Task.Run(async () => await _pollingManager.StartPoll()).GetAwaiter().GetResult(); - } - } - - if (CacheConfig.Enabled) + EnvironmentKey = environmentKey, + ApiUri = new Uri(apiUrl), + EnvironmentRefreshIntervalSeconds = environmentRefreshIntervalSeconds, + EnableLocalEvaluation = enableClientSideEvaluation, + Logger = logger, + EnableAnalytics = enableAnalytics, + RequestTimeout = requestTimeout, + Retries = retries, + CustomHeaders = customHeaders, + CacheConfig = cacheConfig ?? new CacheConfig(false), + OfflineMode = offlineMode, + OfflineHandler = offlineHandler, + HttpClient = httpClient, + }; + // The type of defaultFlagHandler in this constructor is `Func?`, but the type of + // IFlagsmithConfiguration.DefaultFlagHandler is `Func` + if (defaultFlagHandler != null) { - _regularFlagListCache = new RegularFlagListCache(new DateTimeProvider(), - CacheConfig.DurationInMinutes); - _flagListCacheDictionary = new ConcurrentDictionary(); + Flag Handler(string s) => (defaultFlagHandler(s) as Flag)!; + _config.DefaultFlagHandler = Handler; } + Initialise(); } /// - /// Create flagsmith client. + /// Creates a Flagsmith client. + /// Deprecated since 7.1.0. Use instead. /// - /// Flagsmith client configuration - /// Http client used for flagsmith-API requests - public FlagsmithClient(IFlagsmithConfiguration configuration, HttpClient? httpClient = null) : this( - configuration.EnvironmentKey, - configuration.ApiUrl, - configuration.Logger, - configuration.DefaultFlagHandler, - configuration.EnableAnalytics, - configuration.EnableClientSideEvaluation, - configuration.EnvironmentRefreshIntervalSeconds, - configuration.CustomHeaders, - configuration.Retries ?? 1, - configuration.RequestTimeout, - httpClient, - configuration.CacheConfig) + [Obsolete("This constructor is deprecated. Use FlagsmithClient(IFlagsmithConfiguration) instead.")] + public FlagsmithClient(IFlagsmithConfiguration configuration, HttpClient httpClient) { + _config = (FlagsmithConfiguration)configuration; + _config.HttpClient = httpClient; + Initialise(); } /// @@ -194,7 +183,7 @@ public FlagsmithClient(IFlagsmithConfiguration configuration, HttpClient? httpCl /// public async Task GetEnvironmentFlags() { - if (CacheConfig.Enabled) + if (_config.CacheConfig.Enabled) { return _regularFlagListCache!.GetLatestFlags(GetFeatureFlagsFromCorrectSource); } @@ -204,7 +193,7 @@ public async Task GetEnvironmentFlags() private async Task GetFeatureFlagsFromCorrectSource() { - return (OfflineMode || EnableClientSideEvaluation) && Environment != null ? GetFeatureFlagsFromDocument() : await GetFeatureFlagsFromApi().ConfigureAwait(false); + return (_config.OfflineMode || _config.EnableLocalEvaluation) && Environment != null ? GetFeatureFlagsFromDocument() : await GetFeatureFlagsFromApi().ConfigureAwait(false); } /// @@ -222,14 +211,14 @@ public async Task GetIdentityFlags(string identifier, List? trai { var identityWrapper = new IdentityWrapper(identifier, traits, transient); - if (CacheConfig.Enabled) + if (_config.CacheConfig.Enabled) { var flagListCache = GetFlagListCacheByIdentity(identityWrapper); return flagListCache.GetLatestFlags(GetIdentityFlagsFromCorrectSource); } - if (this.OfflineMode) + if (_config.OfflineMode) return this.GetIdentityFlagsFromDocument(identifier, traits ?? null); return await GetIdentityFlagsFromCorrectSource(identityWrapper).ConfigureAwait(false); @@ -237,7 +226,7 @@ public async Task GetIdentityFlags(string identifier, List? trai public async Task GetIdentityFlagsFromCorrectSource(IdentityWrapper identityWrapper) { - if ((OfflineMode || EnableClientSideEvaluation) && Environment != null) + if ((_config.OfflineMode || _config.EnableLocalEvaluation) && Environment != null) { return GetIdentityFlagsFromDocument(identityWrapper.Identifier, identityWrapper.Traits); } @@ -252,13 +241,13 @@ public async Task GetIdentityFlagsFromCorrectSource(IdentityWrapper iden public List? GetIdentitySegments(string identifier, List traits) { - if (this.Environment == null) + if (Environment == null) { throw new FlagsmithClientError("Local evaluation required to obtain identity segments."); } IdentityModel identityModel = new IdentityModel { Identifier = identifier, IdentityTraits = traits?.Select(t => new TraitModel { TraitKey = t.GetTraitKey(), TraitValue = t.GetTraitValue() }).ToList() }; - List segmentModels = Evaluator.GetIdentitySegments(this.Environment, identityModel, new List()); + List segmentModels = Evaluator.GetIdentitySegments(Environment, identityModel, new List()); return segmentModels?.Select(t => new Segment(id: t.Id, name: t.Name)).ToList(); } @@ -269,7 +258,7 @@ private IdentityFlagListCache GetFlagListCacheByIdentity(IdentityWrapper identit { return new IdentityFlagListCache(identityWrapper, new DateTimeProvider(), - CacheConfig.DurationInMinutes); + _config.CacheConfig.DurationInMinutes); }); return flagListCache; @@ -279,31 +268,31 @@ private async Task GetJson(HttpMethod method, string url, string? body = { try { - var policy = HttpPolicies.GetRetryPolicyAwaitable(Retries); + var policy = HttpPolicies.GetRetryPolicyAwaitable(_config.Retries); return await (await policy.ExecuteAsync(async () => { HttpRequestMessage request = new HttpRequestMessage(method, url) { Headers = { - { "X-Environment-Key", EnvironmentKey } + { "X-Environment-Key", _config.EnvironmentKey } } }; - CustomHeaders?.ForEach(kvp => request.Headers.Add(kvp.Key, kvp.Value)); + _config.CustomHeaders?.ForEach(kvp => request.Headers.Add(kvp.Key, kvp.Value)); if (body != null) { request.Content = new StringContent(body, Encoding.UTF8, "application/json"); } - var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(RequestTimeout ?? 100)); - HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationTokenSource.Token).ConfigureAwait(false); + var cancellationTokenSource = new CancellationTokenSource(_config.Timeout); + HttpResponseMessage response = await _config.HttpClient.SendAsync(request, cancellationTokenSource.Token).ConfigureAwait(false); return response.EnsureSuccessStatusCode(); }).ConfigureAwait(false)).Content.ReadAsStringAsync().ConfigureAwait(false); } catch (HttpRequestException e) { - Logger?.LogError("\nHTTP Request Exception Caught!"); - Logger?.LogError("Message :{0} ", e.Message); + _config.Logger?.LogError("\nHTTP Request Exception Caught!"); + _config.Logger?.LogError("Message :{0} ", e.Message); throw new FlagsmithAPIError("Unable to get valid response from Flagsmith API"); } catch (TaskCanceledException) @@ -316,14 +305,14 @@ private async Task GetAndUpdateEnvironmentFromApi() { try { - var json = await GetJson(HttpMethod.Get, ApiUrl + "environment-document/").ConfigureAwait(false); + var json = await GetJson(HttpMethod.Get, new Uri(_config.ApiUri, "environment-document/").AbsoluteUri).ConfigureAwait(false); Environment = JsonConvert.DeserializeObject(json); IdentitiesWithOverridesByIdentifier = Environment?.IdentityOverrides != null ? Environment.IdentityOverrides.ToDictionary(identity => identity.Identifier) : new Dictionary(); - Logger?.LogInformation("Local Environment updated: " + json); + _config.Logger?.LogInformation("Local Environment updated: " + json); } catch (FlagsmithAPIError ex) { - Logger?.LogError(ex.Message); + _config.Logger?.LogError(ex.Message); } } @@ -331,10 +320,10 @@ private async Task GetFeatureFlagsFromApi() { try { - string url = ApiUrl.AppendPath("flags"); + string url = new Uri(_config.ApiUri, "flags/").AbsoluteUri; string json = await GetJson(HttpMethod.Get, url).ConfigureAwait(false); var flags = JsonConvert.DeserializeObject>(json)?.ToList(); - return Flags.FromApiFlag(_analyticsProcessor, DefaultFlagHandler, flags); + return Flags.FromApiFlag(_analyticsProcessor, _config.DefaultFlagHandler, flags); } catch (FlagsmithAPIError e) { @@ -342,7 +331,7 @@ private async Task GetFeatureFlagsFromApi() { return this.GetFeatureFlagsFromDocument(); } - return DefaultFlagHandler != null ? Flags.FromApiFlag(_analyticsProcessor, DefaultFlagHandler, null) : throw e; + return _config.DefaultFlagHandler != null ? Flags.FromApiFlag(_analyticsProcessor, _config.DefaultFlagHandler, null) : throw e; } } @@ -351,12 +340,12 @@ private async Task GetIdentityFlagsFromApi(string identity, List try { traits = traits ?? new List(); - var url = ApiUrl.AppendPath("identities"); + var url = new Uri(_config.ApiUri, "/identities/").AbsoluteUri; var jsonBody = JsonConvert.SerializeObject(new { identifier = identity, traits, transient }); var jsonResponse = await GetJson(HttpMethod.Post, url, body: jsonBody).ConfigureAwait(false); var flags = JsonConvert.DeserializeObject(jsonResponse)?.flags?.ToList(); - return Flags.FromApiFlag(_analyticsProcessor, DefaultFlagHandler, flags); + return Flags.FromApiFlag(_analyticsProcessor, _config.DefaultFlagHandler, flags); } catch (FlagsmithAPIError e) { @@ -364,13 +353,13 @@ private async Task GetIdentityFlagsFromApi(string identity, List { return this.GetIdentityFlagsFromDocument(identity, traits); } - return DefaultFlagHandler != null ? Flags.FromApiFlag(_analyticsProcessor, DefaultFlagHandler, null) : throw e; + return _config.DefaultFlagHandler != null ? Flags.FromApiFlag(_analyticsProcessor, _config.DefaultFlagHandler, null) : throw e; } } private IFlags GetFeatureFlagsFromDocument() { - return Flags.FromFeatureStateModel(_analyticsProcessor, DefaultFlagHandler, _engine.GetEnvironmentFeatureStates(Environment)); + return Flags.FromFeatureStateModel(_analyticsProcessor, _config.DefaultFlagHandler, _engine.GetEnvironmentFeatureStates(Environment)); } private IFlags GetIdentityFlagsFromDocument(string identifier, List? traits) @@ -392,11 +381,11 @@ private IFlags GetIdentityFlagsFromDocument(string identifier, List? tra IdentityTraits = traitModels, }; } - return Flags.FromFeatureStateModel(_analyticsProcessor, DefaultFlagHandler, _engine.GetIdentityFeatureStates(Environment, identity), identity.CompositeKey); + return Flags.FromFeatureStateModel(_analyticsProcessor, _config.DefaultFlagHandler, _engine.GetIdentityFeatureStates(Environment, identity), identity.CompositeKey); } public Dictionary aggregatedAnalytics => _analyticsProcessor != null ? _analyticsProcessor.GetAggregatedAnalytics() : new Dictionary(); ~FlagsmithClient() => _pollingManager?.StopPoll(); } -} \ No newline at end of file +} diff --git a/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs b/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs index 6069f30..437b60c 100644 --- a/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs +++ b/Flagsmith.FlagsmithClient/FlagsmithConfiguration.cs @@ -1,29 +1,64 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using OfflineHandler; +using System.Net.Http; namespace Flagsmith { public class FlagsmithConfiguration : IFlagsmithConfiguration { + private static readonly Uri DefaultApiUri = new Uri("https://edge.api.flagsmith.com/api/v1/"); + private Uri _apiUri = DefaultApiUri; + private TimeSpan _timeout; public FlagsmithConfiguration() { ApiUrl = "https://edge.api.flagsmith.com/api/v1/"; EnvironmentKey = string.Empty; EnvironmentRefreshIntervalSeconds = 60; } + /// - /// Override the URL of the Flagsmith API to communicate with. + /// Override the URL of the Flagsmith API to communicate with. + /// Deprecated since 7.1.0. Use instead. /// - public string ApiUrl { get; set; } + [Obsolete("Use ApiUri instead.")] + public string ApiUrl + { + get => _apiUri.ToString(); + set => _apiUri = value.EndsWith("/") ? new Uri(value) : new Uri($"{value}/"); + } + + /// + /// Versioned base Flagsmith API URI to use for all requests. Defaults to + /// https://edge.api.flagsmith.com/api/v1/. + /// new Uri("https://flagsmith.example.com/api/v1/") + /// + public Uri ApiUri + { + get => _apiUri; + set => _apiUri = value; + } + /// /// The environment key obtained from Flagsmith interface. /// public string EnvironmentKey { get; set; } + + /// + /// Enables local evaluation of flags. + /// + [Obsolete("Use EnableLocalEvaluation instead.")] + public bool EnableClientSideEvaluation + { + get => EnableLocalEvaluation; + set => EnableLocalEvaluation = value; + } + /// /// Enables local evaluation of flags. /// - public bool EnableClientSideEvaluation { get; set; } + public bool EnableLocalEvaluation { get; set; } /// /// If using local evaluation, specify the interval period between refreshes of local environment data. /// @@ -31,7 +66,7 @@ public FlagsmithConfiguration() /// /// Callable which will be used in the case where flags cannot be retrieved from the API or a non existent feature is requested. /// - public Func DefaultFlagHandler { get; set; } + public Func? DefaultFlagHandler { get; set; } /// /// Provide logger for logging polling info & errors which is only applicable when client side evalution is enabled and analytics errors. /// @@ -40,10 +75,24 @@ public FlagsmithConfiguration() /// if enabled, sends additional requests to the Flagsmith API to power flag analytics charts. /// public bool EnableAnalytics { get; set; } + /// /// Number of seconds to wait for a request to complete before terminating the request /// - public Double? RequestTimeout { get; set; } + public Double? RequestTimeout + { + get => _timeout.Seconds; + set => _timeout = TimeSpan.FromSeconds(value ?? 100); + } + + /// + /// Timeout duration to use for HTTP requests. + /// + public TimeSpan Timeout + { + get => _timeout; + set => _timeout = value; + } /// /// Total http retries for every failing request before throwing the final error. /// @@ -56,7 +105,22 @@ public FlagsmithConfiguration() /// /// If enabled, the SDK will cache the flags for the duration specified in the CacheConfig /// - public CacheConfig CacheConfig { get; set; } + public CacheConfig CacheConfig { get; set; } = new CacheConfig(false); + + /// + /// Indicates whether the client is in offline mode. + /// + public bool OfflineMode { get; set; } + + /// + /// Handler for offline mode operations. + /// + public BaseOfflineHandler? OfflineHandler { get; set; } + + /// + /// Http client used for flagsmith-API requests. + /// + public HttpClient HttpClient { get; set; } = new HttpClient(); public bool IsValid() { diff --git a/Flagsmith.FlagsmithClient/IFlagsmithConfiguration.cs b/Flagsmith.FlagsmithClient/IFlagsmithConfiguration.cs index 9de9832..e1dfc07 100644 --- a/Flagsmith.FlagsmithClient/IFlagsmithConfiguration.cs +++ b/Flagsmith.FlagsmithClient/IFlagsmithConfiguration.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; +using OfflineHandler; +using System.Net.Http; namespace Flagsmith { + [Obsolete("Use FlagsmithConfiguration instead.")] public interface IFlagsmithConfiguration { /// @@ -61,6 +64,21 @@ public interface IFlagsmithConfiguration /// CacheConfig CacheConfig { get; set; } + /// + /// Indicates whether the client is in offline mode. + /// + bool OfflineMode { get; set; } + + /// + /// Handler for offline mode operations. + /// + BaseOfflineHandler OfflineHandler { get; set; } + + /// + /// HTTP client used for Flagsmith API requests. + /// + HttpClient HttpClient { get; set; } + bool IsValid(); } -} \ No newline at end of file +}