From 755fdc182cda8164fc141192436273dc0dd65dea Mon Sep 17 00:00:00 2001 From: adams85 <31276480+adams85@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:58:54 +0100 Subject: [PATCH] GetAllValueDetails feature (#55) * Adds and implements GetAllValueDetails/GetAllValueDetailsAsync methods (+ revises RolloutEvaluatorExtensions) * Deprecates GetVariationId/GetVariationIdAsync and GetAllVariationId/GetAllVariationIdAsync * Adds tests --- .../BasicConfigCatClientIntegrationTests.cs | 83 +++++++ .../BasicConfigEvaluatorTests.cs | 18 +- .../ConfigCatClientTests.cs | 218 ++++++++++++++++++ .../ConfigEvaluatorTestsBase.cs | 8 +- .../VariationIdEvaluatorTests.cs | 11 +- src/ConfigCatClient/ConfigCatClient.cs | 105 ++++++++- .../Evaluation/RolloutEvaluatorExtensions.cs | 109 ++++----- src/ConfigCatClient/IConfigCatClient.cs | 18 ++ 8 files changed, 471 insertions(+), 99 deletions(-) diff --git a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs index b30fdabc..7871d611 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs @@ -331,6 +331,89 @@ static void Configure(ConfigCatClientOptions options) } } + [DataRow(ClientCreationStrategy.Singleton)] + [DataRow(ClientCreationStrategy.Constructor)] + [DataRow(ClientCreationStrategy.Builder)] + [DataTestMethod] + public void GetAllValueDetails(ClientCreationStrategy creationStrategy) + { + static void Configure(ConfigCatClientOptions options) + { + options.PollingMode = PollingModes.ManualPoll; + options.Logger = consoleLogger; + options.HttpClientHandler = sharedHandler; + } + + using IConfigCatClient client = creationStrategy switch + { + ClientCreationStrategy.Singleton => ConfigCatClient.Get(SDKKEY, Configure), + ClientCreationStrategy.Constructor => new ConfigCatClient(options => { Configure(options); options.SdkKey = SDKKEY; }), + ClientCreationStrategy.Builder => ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(consoleLogger) + .WithManualPoll() + .WithHttpClientHandler(sharedHandler) + .Create(), + _ => throw new ArgumentOutOfRangeException(nameof(creationStrategy)) + }; + + client.ForceRefresh(); + + var flagEvaluatedEvents = new List(); + client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e); + + var detailsList = client.GetAllValueDetails(); + + Assert.AreEqual(16, detailsList.Count); + var details = detailsList.FirstOrDefault(details => details.Key == "stringDefaultCat"); + Assert.IsNotNull(details); + Assert.AreEqual("Cat", details.Value); + Assert.IsFalse(details.IsDefaultValue); + + CollectionAssert.AreEqual(detailsList.ToArray(), flagEvaluatedEvents.Select(e => e.EvaluationDetails).ToArray()); + } + + [DataRow(ClientCreationStrategy.Singleton)] + [DataRow(ClientCreationStrategy.Constructor)] + [DataRow(ClientCreationStrategy.Builder)] + [DataTestMethod] + public async Task GetAllValueDetailsAsync(ClientCreationStrategy creationStrategy) + { + static void Configure(ConfigCatClientOptions options) + { + options.PollingMode = PollingModes.ManualPoll; + options.Logger = consoleLogger; + options.HttpClientHandler = sharedHandler; + } + + using IConfigCatClient client = creationStrategy switch + { + ClientCreationStrategy.Singleton => ConfigCatClient.Get(SDKKEY, Configure), + ClientCreationStrategy.Constructor => new ConfigCatClient(options => { Configure(options); options.SdkKey = SDKKEY; }), + ClientCreationStrategy.Builder => ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(consoleLogger) + .WithManualPoll() + .WithHttpClientHandler(sharedHandler) + .Create(), + _ => throw new ArgumentOutOfRangeException(nameof(creationStrategy)) + }; + + var flagEvaluatedEvents = new List(); + client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e); + + await client.ForceRefreshAsync(); + var detailsList = await client.GetAllValueDetailsAsync(); + + Assert.AreEqual(16, detailsList.Count); + var details = detailsList.FirstOrDefault(details => details.Key == "stringDefaultCat"); + Assert.IsNotNull(details); + Assert.AreEqual("Cat", details.Value); + Assert.IsFalse(details.IsDefaultValue); + + CollectionAssert.AreEqual(detailsList.ToArray(), flagEvaluatedEvents.Select(e => e.EvaluationDetails).ToArray()); + } + private static void GetValueAndAssert(IConfigCatClient client, string key, string defaultValue, string expectedValue) { var flagEvaluatedEvents = new List(); diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs index abdb7f4f..3e6143af 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs @@ -19,7 +19,7 @@ public class BasicConfigEvaluatorTests : ConfigEvaluatorTestsBase [TestMethod] public void GetValue_WithSimpleKey_ShouldReturnCat() { - string actual = configEvaluator.Evaluate(config, "stringDefaultCat", string.Empty, user: null, null, this.logger, out _); + string actual = configEvaluator.Evaluate(config, "stringDefaultCat", string.Empty, user: null, null, this.logger).Value; Assert.AreNotEqual(string.Empty, actual); Assert.AreEqual("Cat", actual); @@ -28,7 +28,7 @@ public void GetValue_WithSimpleKey_ShouldReturnCat() [TestMethod] public void GetValue_WithNonExistingKey_ShouldReturnDefaultValue() { - string actual = configEvaluator.Evaluate(config, "NotExistsKey", "NotExistsValue", user: null, null, this.logger, out _); + string actual = configEvaluator.Evaluate(config, "NotExistsKey", "NotExistsValue", user: null, null, this.logger).Value; Assert.AreEqual("NotExistsValue", actual); } @@ -36,7 +36,7 @@ public void GetValue_WithNonExistingKey_ShouldReturnDefaultValue() [TestMethod] public void GetValue_WithEmptyProjectConfig_ShouldReturnDefaultValue() { - string actual = configEvaluator.Evaluate(new Dictionary(), "stringDefaultCat", "Default", user: null, null, this.logger, out _); + string actual = configEvaluator.Evaluate(new Dictionary(), "stringDefaultCat", "Default", user: null, null, this.logger).Value; Assert.AreEqual("Default", actual); } @@ -49,13 +49,13 @@ public void GetValue_WithUser_ShouldReturnEvaluatedValue() Email = "c@configcat.com", Country = "United Kingdom", Custom = new Dictionary { { "Custom1", "admin" } } - }, null, this.logger, out _); + }, null, this.logger).Value; Assert.AreEqual(3.1415, actual); } - private delegate object EvaluateDelegate(IRolloutEvaluator evaluator, IDictionary settings, string key, object defaultValue, User user, - ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails evaluationDetails); + private delegate EvaluationDetails EvaluateDelegate(IRolloutEvaluator evaluator, IDictionary settings, string key, object defaultValue, User user, + ProjectConfig remoteConfig, ILogger logger); private static readonly MethodInfo evaluateMethodDefinition = new EvaluateDelegate(RolloutEvaluatorExtensions.Evaluate).Method.GetGenericMethodDefinition(); @@ -84,13 +84,10 @@ public void GetValue_WithCompatibleDefaultValue_ShouldSucceed(string key, object null, null, this.logger, - null }; - var actualValue = evaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args); - var evaluationDetails = (EvaluationDetails)args.Last(); + var evaluationDetails = (EvaluationDetails)evaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args); - Assert.AreEqual(expectedValue, actualValue); Assert.AreEqual(expectedValue, evaluationDetails.Value); } @@ -110,7 +107,6 @@ public void GetValue_WithIncompatibleDefaultValueType_ShouldThrowWithImprovedErr null, null, this.logger, - null }; var ex = Assert.ThrowsException(() => diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index f69ace4d..e0b93945 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -648,6 +648,224 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa Assert.AreSame(actual, flagEvaluatedEvents[0].EvaluationDetails); } + [DataRow(false)] + [DataRow(true)] + [DataTestMethod] + public async Task GetAllValueDetails_ShouldReturnCorrectEvaluationDetails(bool isAsync) + { + // Arrange + + const string cacheKey = "123"; + var configJsonFilePath = Path.Combine("data", "sample_variationid_v5.json"); + var timeStamp = DateTime.UtcNow; + + var client = CreateClientWithMockedFetcher(cacheKey, loggerMock, fetcherMock, + onFetch: _ => FetchResult.Success(new ProjectConfig { JsonString = File.ReadAllText(configJsonFilePath), HttpETag = "12345", TimeStamp = timeStamp }), + configServiceFactory: (fetcher, cacheParams, loggerWrapper) => + { + return new ManualPollConfigService(fetcherMock.Object, cacheParams, loggerWrapper); + }, + evaluatorFactory: loggerWrapper => new RolloutEvaluator(loggerWrapper), new Hooks(), + out var configService, out _); + + if (isAsync) + { + await client.ForceRefreshAsync(); + } + else + { + client.ForceRefresh(); + } + + var flagEvaluatedEvents = new List(); + client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e); + + var user = new User("a@configcat.com") { Email = "a@configcat.com" }; + + // Act + + var actual = isAsync + ? await client.GetAllValueDetailsAsync(user) + : client.GetAllValueDetails(user); + + // Assert + + var expected = new[] + { + new { Key = "boolean", Value = (object)true, VariationId = "67787ae4" }, + new { Key = "text", Value = (object)"true", VariationId = "9bdc6a1f" }, + new { Key = "whole", Value = (object)1, VariationId = "ab30533b" }, + new { Key = "decimal", Value = (object)-2147483647.2147484, VariationId = "8f9559cf" }, + }; + + foreach (var expectedItem in expected) + { + var actualDetails = actual.FirstOrDefault(details => details.Key == expectedItem.Key); + + Assert.IsNotNull(actualDetails); + Assert.AreEqual(expectedItem.Value, actualDetails.Value); + Assert.IsFalse(actualDetails.IsDefaultValue); + Assert.AreEqual(expectedItem.VariationId, actualDetails.VariationId); + Assert.AreEqual(timeStamp, actualDetails.FetchTime); + Assert.AreSame(user, actualDetails.User); + Assert.IsNull(actualDetails.ErrorMessage); + Assert.IsNull(actualDetails.ErrorException); + Assert.IsNotNull(actualDetails.MatchedEvaluationRule); + Assert.IsNull(actualDetails.MatchedEvaluationPercentageRule); + + var flagEvaluatedDetails = flagEvaluatedEvents.Select(e => e.EvaluationDetails).FirstOrDefault(details => details.Key == expectedItem.Key); + + Assert.IsNotNull(flagEvaluatedDetails); + Assert.AreSame(actualDetails, flagEvaluatedDetails); + } + } + + [DataRow(false)] + [DataRow(true)] + [DataTestMethod] + public async Task GetAllValueDetails_DeserializeFailed_ShouldReturnWithEmptyArray(bool isAsync) + { + // Arrange + + configServiceMock.Setup(m => m.GetConfig()).Returns(ProjectConfig.Empty); + configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); + var o = new SettingsWithPreferences(); + configDeserializerMock + .Setup(m => m.TryDeserialize(It.IsAny(), It.IsAny(), out o)) + .Returns(false); + + using IConfigCatClient client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object, new Hooks()); + + var flagEvaluatedEvents = new List(); + client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e); + + // Act + + var actual = isAsync + ? await client.GetAllValueDetailsAsync() + : client.GetAllValueDetails(); + + // Assert + + Assert.IsNotNull(actual); + Assert.AreEqual(0, actual.Count); + Assert.AreEqual(0, flagEvaluatedEvents.Count); + loggerMock.Verify(m => m.Error(It.IsAny()), Times.Once); + } + + [DataRow(false)] + [DataRow(true)] + [DataTestMethod] + public async Task GetAllValueDetails_ConfigServiceThrowException_ShouldReturnEmptyEnumerable(bool isAsync) + { + // Arrange + + configServiceMock + .Setup(m => m.GetConfigAsync()) + .Throws(); + + using IConfigCatClient client = new ConfigCatClient(configServiceMock.Object, loggerMock.Object, evaluatorMock.Object, configDeserializerMock.Object, new Hooks()); + + var flagEvaluatedEvents = new List(); + client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e); + + // Act + + var actual = isAsync + ? await client.GetAllValueDetailsAsync() + : client.GetAllValueDetails(); + + // Assert + + Assert.IsNotNull(actual); + Assert.AreEqual(0, actual.Count); + Assert.AreEqual(0, flagEvaluatedEvents.Count); + } + + [DataRow(false)] + [DataRow(true)] + [DataTestMethod] + public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnDefaultValue(bool isAsync) + { + // Arrange + + const string errorMessage = "Error"; + + const string cacheKey = "123"; + var configJsonFilePath = Path.Combine("data", "sample_variationid_v5.json"); + var timeStamp = DateTime.UtcNow; + + evaluatorMock + .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsNotNull())) + .Throws(new ApplicationException(errorMessage)); + + var client = CreateClientWithMockedFetcher(cacheKey, loggerMock, fetcherMock, + onFetch: _ => FetchResult.Success(new ProjectConfig { JsonString = File.ReadAllText(configJsonFilePath), HttpETag = "12345", TimeStamp = timeStamp }), + configServiceFactory: (fetcher, cacheParams, loggerWrapper) => + { + return new ManualPollConfigService(fetcherMock.Object, cacheParams, loggerWrapper); + }, + evaluatorFactory: _ => evaluatorMock.Object, new Hooks(), + out var configService, out _); + + if (isAsync) + { + await client.ForceRefreshAsync(); + } + else + { + client.ForceRefresh(); + } + + var flagEvaluatedEvents = new List(); + var errorEvents = new List(); + client.FlagEvaluated += (s, e) => flagEvaluatedEvents.Add(e); + client.Error += (s, e) => errorEvents.Add(e); + + var user = new User("a@example.com") { Email = "a@example.com" }; + + // Act + + var actual = isAsync + ? await client.GetAllValueDetailsAsync(user) + : client.GetAllValueDetails(user); + + // Assert + + foreach (var key in new[] { "boolean", "text", "whole", "decimal" }) + { + var actualDetails = actual.FirstOrDefault(details => details.Key == key); + + Assert.IsNotNull(actualDetails); + Assert.AreEqual(key, actualDetails.Key); + Assert.IsNull(actualDetails.Value); + Assert.IsTrue(actualDetails.IsDefaultValue); + Assert.IsNull(actualDetails.VariationId); + Assert.AreEqual(timeStamp, actualDetails.FetchTime); + Assert.AreSame(user, actualDetails.User); + Assert.AreEqual(errorMessage, actualDetails?.ErrorMessage); + Assert.IsInstanceOfType(actualDetails.ErrorException, typeof(ApplicationException)); + Assert.IsNull(actualDetails.MatchedEvaluationRule); + Assert.IsNull(actualDetails.MatchedEvaluationPercentageRule); + + var flagEvaluatedDetails = flagEvaluatedEvents.Select(e => e.EvaluationDetails).FirstOrDefault(details => details.Key == key); + + Assert.IsNotNull(flagEvaluatedDetails); + Assert.AreSame(actualDetails, flagEvaluatedDetails); + } + + Assert.AreEqual(1, errorEvents.Count); + var errorEventArgs = errorEvents[0]; + StringAssert.Contains(errorEventArgs.Message, isAsync ? nameof(IConfigCatClient.GetAllValueDetailsAsync) : nameof(IConfigCatClient.GetAllValueDetails)); + Assert.IsInstanceOfType(errorEventArgs.Exception, typeof(AggregateException)); + var actualException = (AggregateException)errorEventArgs.Exception; + Assert.AreEqual(actual.Count, actualException.InnerExceptions.Count); + foreach (var ex in actualException.InnerExceptions) + { + Assert.IsInstanceOfType(ex, typeof(ApplicationException)); + } + } + [TestMethod] public async Task GetAllKeys_ConfigServiceThrowException_ShouldReturnsWithEmptyArray() { diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs index 67d5bdd5..bd6c10cb 100644 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs @@ -34,25 +34,25 @@ protected virtual void AssertValue(string keyName, string expected, User user) if (k.StartsWith("bool")) { - var actual = configEvaluator.Evaluate(config, keyName, false, user, null, logger, out _); + var actual = configEvaluator.Evaluate(config, keyName, false, user, null, logger).Value; Assert.AreEqual(bool.Parse(expected), actual, $"keyName: {keyName} | userId: {user?.Identifier}"); } else if (k.StartsWith("double")) { - var actual = configEvaluator.Evaluate(config, keyName, double.NaN, user, null, logger, out _); + var actual = configEvaluator.Evaluate(config, keyName, double.NaN, user, null, logger).Value; Assert.AreEqual(double.Parse(expected, CultureInfo.InvariantCulture), actual, $"keyName: {keyName} | userId: {user?.Identifier}"); } else if (k.StartsWith("integer")) { - var actual = configEvaluator.Evaluate(config, keyName, int.MinValue, user, null, logger, out _); + var actual = configEvaluator.Evaluate(config, keyName, int.MinValue, user, null, logger).Value; Assert.AreEqual(int.Parse(expected), actual, $"keyName: {keyName} | userId: {user?.Identifier}"); } else { - var actual = configEvaluator.Evaluate(config, keyName, string.Empty, user, null, logger, out _); + var actual = configEvaluator.Evaluate(config, keyName, string.Empty, user, null, logger).Value; Assert.AreEqual(expected, actual, $"keyName: {keyName} | userId: {user?.Identifier}"); } diff --git a/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs b/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs index 3cfd8e05..14c2036e 100644 --- a/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs @@ -15,7 +15,7 @@ public class VariationIdEvaluatorTests : ConfigEvaluatorTestsBase protected override void AssertValue(string keyName, string expected, User user) { - var actual = base.configEvaluator.EvaluateVariationId(base.config, keyName, null, user, null, this.logger, out _); + var actual = base.configEvaluator.EvaluateVariationId(base.config, keyName, null, user, null, this.logger).VariationId; Assert.AreEqual(expected, actual); } @@ -23,7 +23,7 @@ protected override void AssertValue(string keyName, string expected, User user) [TestMethod] public void EvaluateVariationId_WithSimpleKey_ShouldReturnCat() { - string actual = configEvaluator.EvaluateVariationId(base.config, "boolean", string.Empty, user: null, null, this.logger, out _); + string actual = configEvaluator.EvaluateVariationId(base.config, "boolean", string.Empty, user: null, null, this.logger).VariationId; Assert.AreNotEqual(string.Empty, actual); Assert.AreEqual("a0e56eda", actual); @@ -32,7 +32,7 @@ public void EvaluateVariationId_WithSimpleKey_ShouldReturnCat() [TestMethod] public void EvaluateVariationId_WithNonExistingKey_ShouldReturnDefaultValue() { - string actual = configEvaluator.EvaluateVariationId(config, "NotExistsKey", "DefaultVariationId", user: null, null, this.logger, out _); + string actual = configEvaluator.EvaluateVariationId(config, "NotExistsKey", "DefaultVariationId", user: null, null, this.logger).VariationId; Assert.AreEqual("DefaultVariationId", actual); } @@ -40,7 +40,7 @@ public void EvaluateVariationId_WithNonExistingKey_ShouldReturnDefaultValue() [TestMethod] public void EvaluateVariationId_WithEmptyProjectConfig_ShouldReturnDefaultValue() { - string actual = configEvaluator.EvaluateVariationId(new Dictionary(), "stringDefaultCat", "Default", user: null, null, this.logger, out _); + string actual = configEvaluator.EvaluateVariationId(new Dictionary(), "stringDefaultCat", "Default", user: null, null, this.logger).VariationId; Assert.AreEqual("Default", actual); } @@ -58,8 +58,7 @@ public void EvaluateVariationId_WithUser_ShouldReturnEvaluatedValue() Country = "Hungary" }, null, - this.logger, - out _); + this.logger).VariationId; Assert.AreEqual("30ba32b9", actual); } diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index 7eb5db4f..56aea13d 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -9,6 +9,7 @@ using ConfigCat.Client.Configuration; using ConfigCat.Client.Override; using ConfigCat.Client.Utils; +using System.Runtime.InteropServices; namespace ConfigCat.Client { @@ -259,7 +260,8 @@ public T GetValue(string key, T defaultValue, User user = null) { typeof(T).EnsureSupportedSettingClrType(); settings = this.GetSettings(); - value = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails); + evaluationDetails = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log); + value = evaluationDetails.Value; } catch (Exception ex) { @@ -283,7 +285,8 @@ public async Task GetValueAsync(string key, T defaultValue, User user = nu { typeof(T).EnsureSupportedSettingClrType(); settings = await this.GetSettingsAsync().ConfigureAwait(false); - value = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails); + evaluationDetails = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log); + value = evaluationDetails.Value; } catch (Exception ex) { @@ -306,7 +309,7 @@ public EvaluationDetails GetValueDetails(string key, T defaultValue, User { typeof(T).EnsureSupportedSettingClrType(); settings = this.GetSettings(); - this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails); + evaluationDetails = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log); } catch (Exception ex) { @@ -328,7 +331,7 @@ public async Task> GetValueDetailsAsync(string key, T de { typeof(T).EnsureSupportedSettingClrType(); settings = await this.GetSettingsAsync().ConfigureAwait(false); - this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails); + evaluationDetails = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log); } catch (Exception ex) { @@ -387,7 +390,12 @@ public IDictionary GetAllValues(User user = null) try { var settings = this.GetSettings(); - result = this.configEvaluator.EvaluateAll(settings.Value, user, settings.RemoteConfig, this.log, out evaluationDetailsArray); + evaluationDetailsArray = this.configEvaluator.EvaluateAll(settings.Value, user, settings.RemoteConfig, this.log, out var exceptions); + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + result = evaluationDetailsArray.ToDictionary(details => details.Key, details => details.Value); } catch (Exception ex) { @@ -413,7 +421,12 @@ public async Task> GetAllValuesAsync(User user = nul try { var settings = await this.GetSettingsAsync().ConfigureAwait(false); - result = this.configEvaluator.EvaluateAll(settings.Value, user, settings.RemoteConfig, this.log, out evaluationDetailsArray); + evaluationDetailsArray = this.configEvaluator.EvaluateAll(settings.Value, user, settings.RemoteConfig, this.log, out var exceptions); + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + result = evaluationDetailsArray.ToDictionary(details => details.Key, details => details.Value); } catch (Exception ex) { @@ -430,6 +443,62 @@ public async Task> GetAllValuesAsync(User user = nul return result; } + /// + public IReadOnlyList GetAllValueDetails(User user = null) + { + EvaluationDetails[] evaluationDetailsArray = null; + user ??= this.defaultUser; + try + { + var settings = this.GetSettings(); + evaluationDetailsArray = this.configEvaluator.EvaluateAll(settings.Value, user, settings.RemoteConfig, this.log, out var exceptions); + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + } + catch (Exception ex) + { + this.log.Error($"Error occured in '{nameof(GetAllValueDetails)}' method.", ex); + evaluationDetailsArray ??= ArrayUtils.EmptyArray(); + } + + foreach (var evaluationDetails in evaluationDetailsArray) + { + this.hooks.RaiseFlagEvaluated(evaluationDetails); + } + + return evaluationDetailsArray; + } + + /// + public async Task> GetAllValueDetailsAsync(User user = null) + { + EvaluationDetails[] evaluationDetailsArray = null; + user ??= this.defaultUser; + try + { + var settings = await this.GetSettingsAsync().ConfigureAwait(false); + evaluationDetailsArray = this.configEvaluator.EvaluateAll(settings.Value, user, settings.RemoteConfig, this.log, out var exceptions); + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + } + catch (Exception ex) + { + this.log.Error($"Error occured in '{nameof(GetAllValueDetailsAsync)}' method.", ex); + evaluationDetailsArray ??= ArrayUtils.EmptyArray(); + } + + foreach (var evaluationDetails in evaluationDetailsArray) + { + this.hooks.RaiseFlagEvaluated(evaluationDetails); + } + + return evaluationDetailsArray; + } + /// public RefreshResult ForceRefresh() { @@ -545,6 +614,7 @@ public static void DisposeAll() } /// + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetValueDetails() method instead.")] public string GetVariationId(string key, string defaultVariationId, User user = null) { string variationId; @@ -554,7 +624,8 @@ public string GetVariationId(string key, string defaultVariationId, User user = try { settings = this.GetSettings(); - variationId = this.configEvaluator.EvaluateVariationId(settings.Value, key, defaultVariationId, user, settings.RemoteConfig, this.log, out evaluationDetails); + evaluationDetails = this.configEvaluator.EvaluateVariationId(settings.Value, key, defaultVariationId, user, settings.RemoteConfig, this.log); + variationId = evaluationDetails.VariationId; } catch (Exception ex) { @@ -568,6 +639,7 @@ public string GetVariationId(string key, string defaultVariationId, User user = } /// + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetValueDetailsAsync() method instead.")] public async Task GetVariationIdAsync(string key, string defaultVariationId, User user = null) { string variationId; @@ -577,7 +649,8 @@ public async Task GetVariationIdAsync(string key, string defaultVariatio try { settings = await this.GetSettingsAsync().ConfigureAwait(false); - variationId = this.configEvaluator.EvaluateVariationId(settings.Value, key, defaultVariationId, user, settings.RemoteConfig, this.log, out evaluationDetails); + evaluationDetails = this.configEvaluator.EvaluateVariationId(settings.Value, key, defaultVariationId, user, settings.RemoteConfig, this.log); + variationId = evaluationDetails.VariationId; } catch (Exception ex) { @@ -591,6 +664,7 @@ public async Task GetVariationIdAsync(string key, string defaultVariatio } /// + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetAllValueDetails() method instead.")] public IEnumerable GetAllVariationId(User user = null) { IEnumerable result; @@ -599,7 +673,12 @@ public IEnumerable GetAllVariationId(User user = null) try { var settings = this.GetSettings(); - result = this.configEvaluator.EvaluateAllVariationIds(settings.Value, user, settings.RemoteConfig, this.log, out evaluationDetailsArray); + evaluationDetailsArray = this.configEvaluator.EvaluateAllVariationIds(settings.Value, user, settings.RemoteConfig, this.log, out var exceptions); + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + result = evaluationDetailsArray.Select(details => details.VariationId).Where(variationId => variationId is not null).ToArray(); } catch (Exception ex) { @@ -617,6 +696,7 @@ public IEnumerable GetAllVariationId(User user = null) } /// + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetAllValueDetailsAsync() method instead.")] public async Task> GetAllVariationIdAsync(User user = null) { IEnumerable result; @@ -625,7 +705,12 @@ public async Task> GetAllVariationIdAsync(User user = null) try { var settings = await this.GetSettingsAsync().ConfigureAwait(false); - result = this.configEvaluator.EvaluateAllVariationIds(settings.Value, user, settings.RemoteConfig, this.log, out evaluationDetailsArray); + evaluationDetailsArray = this.configEvaluator.EvaluateAllVariationIds(settings.Value, user, settings.RemoteConfig, this.log, out var exceptions); + if (exceptions is { Count: > 0 }) + { + throw new AggregateException(exceptions); + } + result = evaluationDetailsArray.Select(details => details.VariationId).Where(variationId => variationId is not null).ToArray(); } catch (Exception ex) { diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 1f9a9163..7813ebdb 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -7,15 +7,14 @@ namespace ConfigCat.Client.Evaluation { internal static class RolloutEvaluatorExtensions { - public static T Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, T defaultValue, User user, - ProjectConfig remoteConfig, out EvaluationDetails evaluationDetails) + public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, T defaultValue, User user, + ProjectConfig remoteConfig) { - evaluationDetails = (EvaluationDetails)evaluator.Evaluate(setting, key, defaultValue?.ToString(), user, remoteConfig, static (settingType, value) => EvaluationDetails.Create(settingType, value)); - return evaluationDetails.Value; + return (EvaluationDetails)evaluator.Evaluate(setting, key, defaultValue?.ToString(), user, remoteConfig, static (settingType, value) => EvaluationDetails.Create(settingType, value)); } - public static T Evaluate(this IRolloutEvaluator evaluator, IDictionary settings, string key, T defaultValue, User user, - ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails evaluationDetails) + public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, IDictionary settings, string key, T defaultValue, User user, + ProjectConfig remoteConfig, ILogger logger) { string errorMessage; @@ -23,79 +22,67 @@ public static T Evaluate(this IRolloutEvaluator evaluator, IDictionary EvaluationDetails.Create(settingType, value)); - return evaluationDetails.Value; + return evaluator.Evaluate(setting, key, defaultValue?.ToString(), user, remoteConfig, static (settingType, value) => EvaluationDetails.Create(settingType, value)); } - public static IDictionary EvaluateAll(this IRolloutEvaluator evaluator, IDictionary settings, User user, - ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails[] evaluationDetailsArray) + public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, IDictionary settings, User user, + ProjectConfig remoteConfig, ILogger logger, out IReadOnlyList exceptions) { if (!CheckSettingsAvailable(settings, logger)) { - evaluationDetailsArray = ArrayUtils.EmptyArray(); - return new Dictionary(); + exceptions = null; + return ArrayUtils.EmptyArray(); } - var result = new Dictionary(settings.Count); - evaluationDetailsArray = new EvaluationDetails[settings.Count]; - List exceptions = null; + var evaluationDetailsArray = new EvaluationDetails[settings.Count]; + List exceptionList = null; int index = 0; foreach (var kvp in settings) { - object value; EvaluationDetails evaluationDetails; try { - value = evaluator.Evaluate(kvp.Value, kvp.Key, defaultValue: null, user, remoteConfig, out evaluationDetails); + evaluationDetails = evaluator.Evaluate(kvp.Value, kvp.Key, defaultValue: null, user, remoteConfig); } catch (Exception ex) { - exceptions ??= new List(); - exceptions.Add(ex); + exceptionList ??= new List(); + exceptionList.Add(ex); evaluationDetails = EvaluationDetails.FromDefaultValue(kvp.Key, defaultValue: (object)null, fetchTime: remoteConfig?.TimeStamp, user, ex.Message, ex); - value = null; } evaluationDetailsArray[index++] = evaluationDetails; - result.Add(kvp.Key, value); } - if (exceptions is not null) - { - throw new AggregateException(exceptions); - } - - return result; + exceptions = exceptionList; + return evaluationDetailsArray; } - public static string EvaluateVariationId(this IRolloutEvaluator evaluator, Setting setting, string key, string defaultVariationId, User user, - ProjectConfig remoteConfig, out EvaluationDetails evaluationDetails) + public static EvaluationDetails EvaluateVariationId(this IRolloutEvaluator evaluator, Setting setting, string key, string defaultVariationId, User user, + ProjectConfig remoteConfig) { - evaluationDetails = evaluator.EvaluateVariationId(setting, key, defaultVariationId, user, remoteConfig, static (settingType, value) => EvaluationDetails.Create(settingType, value)); - return evaluationDetails.VariationId; + return evaluator.EvaluateVariationId(setting, key, defaultVariationId, user, remoteConfig, static (settingType, value) => EvaluationDetails.Create(settingType, value)); } - public static string EvaluateVariationId(this IRolloutEvaluator evaluator, IDictionary settings, string key, string defaultVariationId, User user, - ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails evaluationDetails) + public static EvaluationDetails EvaluateVariationId(this IRolloutEvaluator evaluator, IDictionary settings, string key, string defaultVariationId, User user, + ProjectConfig remoteConfig, ILogger logger) { string errorMessage; @@ -103,65 +90,51 @@ public static string EvaluateVariationId(this IRolloutEvaluator evaluator, IDict { errorMessage = $"Config JSON is not present. Returning the {nameof(defaultVariationId)} defined in the app source code: '{defaultVariationId}'."; logger.Error(errorMessage); - evaluationDetails = EvaluationDetails.FromDefaultVariationId(key, defaultVariationId, fetchTime: remoteConfig?.TimeStamp, user, errorMessage); - return defaultVariationId; + return EvaluationDetails.FromDefaultVariationId(key, defaultVariationId, fetchTime: remoteConfig?.TimeStamp, user, errorMessage); } if (!settings.TryGetValue(key, out var setting)) { errorMessage = $"Evaluating '{key}' failed (key was not found in config JSON). Returning the {nameof(defaultVariationId)} that you specified in the source code: '{defaultVariationId}'. These are the available keys: {KeysToString(settings)}."; logger.Error(errorMessage); - evaluationDetails = EvaluationDetails.FromDefaultVariationId(key, defaultVariationId, fetchTime: remoteConfig?.TimeStamp, user, errorMessage); - return defaultVariationId; + return EvaluationDetails.FromDefaultVariationId(key, defaultVariationId, fetchTime: remoteConfig?.TimeStamp, user, errorMessage); } - return evaluator.EvaluateVariationId(setting, key, defaultVariationId, user, remoteConfig, out evaluationDetails); + return evaluator.EvaluateVariationId(setting, key, defaultVariationId, user, remoteConfig); } - public static IList EvaluateAllVariationIds(this IRolloutEvaluator evaluator, IDictionary settings, User user, - ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails[] evaluationDetailsArray) + public static EvaluationDetails[] EvaluateAllVariationIds(this IRolloutEvaluator evaluator, IDictionary settings, User user, + ProjectConfig remoteConfig, ILogger logger, out IReadOnlyList exceptions) { if (!CheckSettingsAvailable(settings, logger)) { - evaluationDetailsArray = ArrayUtils.EmptyArray(); - return ArrayUtils.EmptyArray(); + exceptions = null; + return ArrayUtils.EmptyArray(); } - var result = new List(settings.Count); - evaluationDetailsArray = new EvaluationDetails[settings.Count]; - List exceptions = null; + var evaluationDetailsArray = new EvaluationDetails[settings.Count]; + List exceptionList = null; int index = 0; foreach (var kvp in settings) { - string variationId; EvaluationDetails evaluationDetails; try { - variationId = evaluator.EvaluateVariationId(kvp.Value, kvp.Key, defaultVariationId: null, user, remoteConfig, out evaluationDetails); + evaluationDetails = evaluator.EvaluateVariationId(kvp.Value, kvp.Key, defaultVariationId: null, user, remoteConfig); } catch (Exception ex) { - exceptions ??= new List(); - exceptions.Add(ex); + exceptionList ??= new List(); + exceptionList.Add(ex); evaluationDetails = EvaluationDetails.FromDefaultVariationId(kvp.Key, defaultVariationId: null, fetchTime: remoteConfig?.TimeStamp, user, ex.Message, ex); - variationId = null; } evaluationDetailsArray[index++] = evaluationDetails; - if (variationId is not null) - { - result.Add(variationId); - } - } - - if (exceptions is not null) - { - throw new AggregateException(exceptions); } - result.TrimExcess(); - return result; + exceptions = exceptionList; + return evaluationDetailsArray; } internal static bool CheckSettingsAvailable(IDictionary settings, ILogger logger) diff --git a/src/ConfigCatClient/IConfigCatClient.cs b/src/ConfigCatClient/IConfigCatClient.cs index 54a04169..c6345b68 100644 --- a/src/ConfigCatClient/IConfigCatClient.cs +++ b/src/ConfigCatClient/IConfigCatClient.cs @@ -80,6 +80,20 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable /// The key-value collection. Task> GetAllValuesAsync(User user = null); + /// + /// Returns the values along with evaluation details of all feature flags and settings synchronously. + /// + /// The user object for variation evaluation. + /// The key-value collection. + IReadOnlyList GetAllValueDetails(User user = null); + + /// + /// Returns the values along with evaluation details of all feature flags and settings asynchronously. + /// + /// The user object for variation evaluation. + /// The key-value collection. + Task> GetAllValueDetailsAsync(User user = null); + /// /// Refreshes the configuration. /// @@ -97,6 +111,7 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable /// In case of failure return this value. /// The user object for variation evaluation. /// Variation ID. + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetValueDetails() method instead.")] string GetVariationId(string key, string defaultVariationId, User user = null); /// @@ -106,6 +121,7 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable /// In case of failure return this value. /// The user object for variation evaluation. /// Variation ID. + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetValueDetailsAsync() method instead.")] Task GetVariationIdAsync(string key, string defaultVariationId, User user = null); /// @@ -113,6 +129,7 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable /// /// The user object for variation evaluation. /// Collection of all Variation IDs. + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetAllValueDetails() method instead.")] IEnumerable GetAllVariationId(User user = null); /// @@ -120,6 +137,7 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable /// /// The user object for variation evaluation. /// Collection of all Variation IDs. + [Obsolete("This method is obsolete and will be removed from the public API in a future major version. Please use the GetAllValueDetailsAsync() method instead.")] Task> GetAllVariationIdAsync(User user = null); ///