diff --git a/appveyor.yml b/appveyor.yml index 2537d76b..5ebda5c4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - build_version: 8.1.0 + build_version: 8.1.1 version: $(build_version)-{build} image: Visual Studio 2022 configuration: Release diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index ecef4e4e..fd4f5f5d 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -150,7 +150,7 @@ public void GetValue_EvaluateServiceThrowException_ShouldReturnDefaultValue() const string defaultValue = "Victory for the Firstborn!"; this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null, It.IsAny(), It.IsAny())) + .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -179,7 +179,7 @@ public async Task GetValueAsync_EvaluateServiceThrowException_ShouldReturnDefaul const string defaultValue = "Victory for the Firstborn!"; this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null, It.IsAny(), It.IsAny())) + .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, null)) .Throws(); var client = new ConfigCatClient(this.configServiceMock.Object, this.loggerMock.Object, this.evaluatorMock.Object, new Hooks()); @@ -463,7 +463,7 @@ public async Task GetValueDetails_EvaluateServiceThrowException_ShouldReturnDefa var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, It.IsAny(), It.IsAny(), It.IsNotNull())) + .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), defaultValue, It.IsAny())) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, @@ -658,7 +658,7 @@ public async Task GetAllValueDetails_EvaluateServiceThrowException_ShouldReturnD var timeStamp = ProjectConfig.GenerateTimeStamp(); this.evaluatorMock - .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsNotNull())) + .Setup(m => m.Evaluate(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new ApplicationException(errorMessage)); var client = CreateClientWithMockedFetcher(cacheKey, this.loggerMock, this.fetcherMock, diff --git a/src/ConfigCat.Client.Tests/OverrideTests.cs b/src/ConfigCat.Client.Tests/OverrideTests.cs index 7f758dac..24189aab 100644 --- a/src/ConfigCat.Client.Tests/OverrideTests.cs +++ b/src/ConfigCat.Client.Tests/OverrideTests.cs @@ -1,9 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +#if USE_NEWTONSOFT_JSON +using JsonValue = Newtonsoft.Json.Linq.JValue; +#else +using JsonValue = System.Text.Json.JsonElement; +#endif + namespace ConfigCat.Client.Tests; [TestClass] @@ -511,8 +518,16 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_Dictionary(object .MakeGenericMethod(defaultValue.GetType()); var actualEvaluatedValue = method.Invoke(client, new[] { key, defaultValue, null }); + var actualEvaluatedValues = client.GetAllValues(user: null); Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue); + + var overrideValueSettingType = overrideValue.DetermineSettingType(); + var expectedEvaluatedValues = new KeyValuePair[] + { + new(key, overrideValueSettingType != SettingType.Unknown ? overrideValue : null) + }; + CollectionAssert.AreEquivalent(expectedEvaluatedValues, actualEvaluatedValues.ToArray()); } [DataRow("true", false, true)] @@ -542,6 +557,13 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_Dictionary(object public void OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig(string overrideValueJson, object defaultValue, object expectedEvaluatedValue) { const string key = "flag"; + var overrideValue = +#if USE_NEWTONSOFT_JSON + overrideValueJson.Deserialize(); +#else + overrideValueJson.Deserialize(); +#endif + var filePath = Path.GetTempFileName(); File.WriteAllText(filePath, $"{{ \"flags\": {{ \"{key}\": {overrideValueJson} }} }}"); @@ -559,8 +581,17 @@ public void OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig(s .MakeGenericMethod(defaultValue.GetType()); var actualEvaluatedValue = method.Invoke(client, new[] { key, defaultValue, null }); + var actualEvaluatedValues = client.GetAllValues(user: null); Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue); + var overrideValueSettingType = overrideValue.DetermineSettingType(); + var expectedEvaluatedValues = new KeyValuePair[] + { + new(key, overrideValueSettingType != SettingType.Unknown + ? (overrideValue is JsonValue jsonValue ? jsonValue.ConvertToObject(overrideValueSettingType) : overrideValue) + : null) + }; + CollectionAssert.AreEquivalent(expectedEvaluatedValues, actualEvaluatedValues.ToArray()); } finally { diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index 2cd7cba9..1636901b 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -419,7 +419,6 @@ public async Task> GetAllKeysAsync(CancellationToken public IReadOnlyDictionary GetAllValues(User? user = null) { const string defaultReturnValue = "empty dictionary"; - IReadOnlyDictionary result; EvaluationDetails[]? evaluationDetailsArray = null; user ??= this.defaultUser; try @@ -430,15 +429,15 @@ public async Task> GetAllKeysAsync(CancellationToken { throw new AggregateException(exceptions); } - result = evaluationDetailsArray.ToDictionary(details => details.Key, details => details.Value); } catch (Exception ex) { this.logger.SettingEvaluationError(nameof(GetAllValues), defaultReturnValue, ex); evaluationDetailsArray ??= ArrayUtils.EmptyArray(); - result = new Dictionary(); } + var result = evaluationDetailsArray.ToDictionary(details => details.Key, details => details.Value); + foreach (var evaluationDetails in evaluationDetailsArray) { this.hooks.RaiseFlagEvaluated(evaluationDetails); @@ -451,7 +450,6 @@ public async Task> GetAllKeysAsync(CancellationToken public async Task> GetAllValuesAsync(User? user = null, CancellationToken cancellationToken = default) { const string defaultReturnValue = "empty dictionary"; - IReadOnlyDictionary result; EvaluationDetails[]? evaluationDetailsArray = null; user ??= this.defaultUser; try @@ -462,7 +460,6 @@ public async Task> GetAllKeysAsync(CancellationToken { throw new AggregateException(exceptions); } - result = evaluationDetailsArray.ToDictionary(details => details.Key, details => details.Value); } catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken) { @@ -472,9 +469,10 @@ public async Task> GetAllKeysAsync(CancellationToken { this.logger.SettingEvaluationError(nameof(GetAllValuesAsync), defaultReturnValue, ex); evaluationDetailsArray ??= ArrayUtils.EmptyArray(); - result = new Dictionary(); } + var result = evaluationDetailsArray.ToDictionary(details => details.Key, details => details.Value); + foreach (var evaluationDetails in evaluationDetailsArray) { this.hooks.RaiseFlagEvaluated(evaluationDetails); diff --git a/src/ConfigCatClient/Evaluation/EvaluateResult.cs b/src/ConfigCatClient/Evaluation/EvaluateResult.cs new file mode 100644 index 00000000..acd33a95 --- /dev/null +++ b/src/ConfigCatClient/Evaluation/EvaluateResult.cs @@ -0,0 +1,23 @@ +#if USE_NEWTONSOFT_JSON +using JsonValue = Newtonsoft.Json.Linq.JValue; +#else +using JsonValue = System.Text.Json.JsonElement; +#endif + +namespace ConfigCat.Client.Evaluation; + +internal readonly struct EvaluateResult +{ + public EvaluateResult(JsonValue value, string? variationId, RolloutRule? matchedTargetingRule = null, RolloutPercentageItem? matchedPercentageOption = null) + { + Value = value; + VariationId = variationId; + MatchedTargetingRule = matchedTargetingRule; + MatchedPercentageOption = matchedPercentageOption; + } + + public JsonValue Value { get; } + public string? VariationId { get; } + public RolloutRule? MatchedTargetingRule { get; } + public RolloutPercentageItem? MatchedPercentageOption { get; } +} diff --git a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs index 68acdb43..5a87eb95 100644 --- a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs +++ b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using JsonValue = Newtonsoft.Json.Linq.JValue; @@ -9,23 +10,13 @@ namespace ConfigCat.Client; -internal delegate EvaluationDetails EvaluationDetailsFactory(Setting setting, JsonValue value); - /// /// The evaluated value and additional information about the evaluation of a feature flag or setting. /// public abstract record class EvaluationDetails { - private static EvaluationDetails Create(JsonValue value) + private static void EnsureValidSettingValue(JsonValue value, ref SettingType settingType, string? unsupportedTypeError) { - return new EvaluationDetails { Value = value.ConvertTo() }; - } - - internal static EvaluationDetails Create(SettingType settingType, JsonValue value, string? unsupportedTypeError) - { - // NOTE: We've already checked earlier in the call chain that TValue is an allowed type (see also TypeExtensions.EnsureSupportedSettingClrType). - Debug.Assert(typeof(TValue) == typeof(object) || typeof(TValue).ToSettingType() != SettingType.Unknown, "Type is not supported."); - // Setting type is not known (it's not present in the config JSON, it's an unsupported value coming from a flag override, etc.)? if (settingType == SettingType.Unknown) { @@ -37,6 +28,23 @@ internal static EvaluationDetails Create(SettingType settingType throw new ArgumentException(unsupportedTypeError ?? $"Setting value '{value}' is of an unsupported type.", nameof(value)); } } + } + + private static EvaluationDetails Create(string key, JsonValue value) + { + return new EvaluationDetails(key, value.ConvertTo()); + } + + internal static EvaluationDetails FromEvaluateResult(string key, in EvaluateResult evaluateResult, SettingType settingType, string? unsupportedTypeError, + DateTime? fetchTime, User? user) + { + // NOTE: We've already checked earlier in the call chain that TValue is an allowed type (see also TypeExtensions.EnsureSupportedSettingClrType). + Debug.Assert(typeof(TValue) == typeof(object) || typeof(TValue).ToSettingType() != SettingType.Unknown, "Type is not supported."); + + var value = evaluateResult.Value; + EnsureValidSettingValue(value, ref settingType, unsupportedTypeError); + + EvaluationDetails instance; if (typeof(TValue) != typeof(object)) { @@ -45,60 +53,42 @@ internal static EvaluationDetails Create(SettingType settingType throw new InvalidOperationException($"The type of a setting must match the type of the setting's default value.{Environment.NewLine}Setting's type was {settingType} but the default value's type was {typeof(TValue)}.{Environment.NewLine}Please use a default value which corresponds to the setting type {settingType}."); } - return Create(value); + instance = Create(key, value); } else { - EvaluationDetails evaluationDetails = new EvaluationDetails { Value = value.ConvertToObject(settingType) }; - return (EvaluationDetails)evaluationDetails; + EvaluationDetails evaluationDetails = new EvaluationDetails(key, value.ConvertToObject(settingType)); + instance = (EvaluationDetails)evaluationDetails; } - } - internal static EvaluationDetails Create(SettingType settingType, JsonValue value) - { - return settingType switch - { - SettingType.Boolean => Create(value), - SettingType.String => Create(value), - SettingType.Int => Create(value), - SettingType.Double => Create(value), - _ => throw new ArgumentOutOfRangeException(nameof(settingType), settingType, null) - }; + instance.Initialize(evaluateResult, fetchTime, user); + return instance; } - internal static EvaluationDetails FromJsonValue( - EvaluationDetailsFactory factory, - Setting setting, - string key, - JsonValue value, - string? variationId, - DateTime? fetchTime, - User? user, - RolloutRule? matchedEvaluationRule = null, - RolloutPercentageItem? matchedEvaluationPercentageRule = null) + internal static EvaluationDetails FromEvaluateResult(string key, in EvaluateResult evaluateResult, SettingType settingType, string? unsupportedTypeError, + DateTime? fetchTime, User? user) { - var instance = factory(setting, value); + var value = evaluateResult.Value; + EnsureValidSettingValue(value, ref settingType, unsupportedTypeError); - instance.Key = key; - instance.VariationId = variationId; - if (fetchTime is not null) + EvaluationDetails instance = settingType switch { - instance.FetchTime = fetchTime.Value; - } - instance.User = user; - instance.MatchedEvaluationRule = matchedEvaluationRule; - instance.MatchedEvaluationPercentageRule = matchedEvaluationPercentageRule; + SettingType.Boolean => Create(key, value), + SettingType.String => Create(key, value), + SettingType.Int => Create(key, value), + SettingType.Double => Create(key, value), + _ => throw new ArgumentOutOfRangeException(nameof(settingType), settingType, null) + }; + instance.Initialize(evaluateResult, fetchTime, user); return instance; } internal static EvaluationDetails FromDefaultValue(string key, TValue defaultValue, DateTime? fetchTime, User? user, string? errorMessage = null, Exception? errorException = null) { - var instance = new EvaluationDetails + var instance = new EvaluationDetails(key, defaultValue) { - Key = key, - Value = defaultValue, User = user, IsDefaultValue = true, ErrorMessage = errorMessage, @@ -118,6 +108,18 @@ private protected EvaluationDetails(string key) Key = key; } + private void Initialize(in EvaluateResult evaluateResult, DateTime? fetchTime, User? user) + { + VariationId = evaluateResult.VariationId; + if (fetchTime is not null) + { + FetchTime = fetchTime.Value; + } + User = user; + MatchedEvaluationRule = evaluateResult.MatchedTargetingRule; + MatchedEvaluationPercentageRule = evaluateResult.MatchedPercentageOption; + } + /// /// Key of the feature flag or setting. /// @@ -175,8 +177,6 @@ private protected EvaluationDetails(string key) /// public sealed record class EvaluationDetails : EvaluationDetails { - internal EvaluationDetails() : this(key: null!, value: default!) { } - /// /// Initializes a new instance of the class. /// diff --git a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs index d122adb8..37b1d355 100644 --- a/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/IRolloutEvaluator.cs @@ -2,6 +2,5 @@ namespace ConfigCat.Client.Evaluation; internal interface IRolloutEvaluator { - EvaluationDetails Evaluate(Setting setting, string key, string? logDefaultValue, User? user, - ProjectConfig? remoteConfig, EvaluationDetailsFactory detailsFactory); + EvaluateResult Evaluate(Setting setting, string key, string? logDefaultValue, User? user); } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 6fac79c8..56de299e 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -5,11 +5,6 @@ using System.Linq; using static System.FormattableString; -#if USE_NEWTONSOFT_JSON -using JsonValue = Newtonsoft.Json.Linq.JValue; -#else -using JsonValue = System.Text.Json.JsonElement; -#endif namespace ConfigCat.Client.Evaluation; @@ -22,8 +17,7 @@ public RolloutEvaluator(LoggerWrapper logger) this.logger = logger; } - public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaultValue, User? user, - ProjectConfig? remoteConfig, EvaluationDetailsFactory detailsFactory) + public EvaluateResult Evaluate(Setting setting, string key, string? logDefaultValue, User? user) { var evaluateLog = new EvaluateLogger { @@ -35,42 +29,28 @@ public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaul try { + EvaluateResult evaluateResult; + if (user is not null) { // evaluate targeting rules - if (TryEvaluateRules(setting.RolloutRules, user, evaluateLog, out var evaluateRulesResult)) + if (TryEvaluateRules(setting.RolloutRules, user, evaluateLog, out evaluateResult)) { - evaluateLog.ReturnValue = evaluateRulesResult.Value.ToString(); - evaluateLog.VariationId = evaluateRulesResult.VariationId; - - return EvaluationDetails.FromJsonValue( - detailsFactory, - setting, - key, - evaluateRulesResult.Value, - evaluateRulesResult.VariationId, - fetchTime: remoteConfig?.TimeStamp, - user, - matchedEvaluationRule: evaluateRulesResult.MatchedRule); + evaluateLog.ReturnValue = evaluateResult.Value.ToString(); + evaluateLog.VariationId = evaluateResult.VariationId; + + return evaluateResult; } // evaluate percentage options - if (TryEvaluatePercentageRules(setting.RolloutPercentageItems, key, user, evaluateLog, out var evaluatePercentageRulesResult)) + if (TryEvaluatePercentageRules(setting.RolloutPercentageItems, key, user, evaluateLog, out evaluateResult)) { - evaluateLog.ReturnValue = evaluatePercentageRulesResult.Value.ToString(); - evaluateLog.VariationId = evaluatePercentageRulesResult.VariationId; - - return EvaluationDetails.FromJsonValue( - detailsFactory, - setting, - key, - evaluatePercentageRulesResult.Value, - evaluatePercentageRulesResult.VariationId, - fetchTime: remoteConfig?.TimeStamp, - user, - matchedEvaluationPercentageRule: evaluatePercentageRulesResult.MatchedRule); + evaluateLog.ReturnValue = evaluateResult.Value.ToString(); + evaluateLog.VariationId = evaluateResult.VariationId; + + return evaluateResult; } } else if (setting.RolloutRules.Any() || setting.RolloutPercentageItems.Any()) @@ -83,14 +63,8 @@ public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaul evaluateLog.ReturnValue = setting.Value.ToString(); evaluateLog.VariationId = setting.VariationId; - return EvaluationDetails.FromJsonValue( - detailsFactory, - setting, - key, - setting.Value, - setting.VariationId, - fetchTime: remoteConfig?.TimeStamp, - user); + evaluateResult = new EvaluateResult(setting.Value, setting.VariationId); + return evaluateResult; } finally { @@ -98,7 +72,7 @@ public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaul } } - private static bool TryEvaluatePercentageRules(ICollection rolloutPercentageItems, string key, User user, EvaluateLogger evaluateLog, out EvaluateResult result) + private static bool TryEvaluatePercentageRules(ICollection rolloutPercentageItems, string key, User user, EvaluateLogger evaluateLog, out EvaluateResult result) { if (rolloutPercentageItems.Count > 0) { @@ -120,7 +94,7 @@ private static bool TryEvaluatePercentageRules(ICollection {hashScale} THEN '{percentageRule.Value}'] => no match")); continue; } - result = new EvaluateResult(percentageRule.Value, percentageRule.VariationId, percentageRule); + result = new EvaluateResult(percentageRule.Value, percentageRule.VariationId, matchedPercentageOption: percentageRule); evaluateLog.Log(Invariant($" - % option: [IF {bucket} > {hashScale} THEN '{percentageRule.Value}'] => MATCH, applying % option")); return true; } @@ -130,14 +104,14 @@ private static bool TryEvaluatePercentageRules(ICollection rules, User user, EvaluateLogger logger, out EvaluateResult result) + private static bool TryEvaluateRules(ICollection rules, User user, EvaluateLogger logger, out EvaluateResult result) { if (rules.Count > 0) { logger.Log(Invariant($"Applying the first targeting rule that matches the User '{user.Serialize()}':")); foreach (var rule in rules.OrderBy(o => o.Order)) { - result = new EvaluateResult(rule.Value, rule.VariationId, rule); + result = new EvaluateResult(rule.Value, rule.VariationId, matchedTargetingRule: rule); var l = Invariant($" - rule: [IF User.{rule.ComparisonAttribute} {RolloutRule.FormatComparator(rule.Comparator)} '{rule.ComparisonValue}' THEN {rule.Value}] => "); if (!user.AllAttributes.ContainsKey(rule.ComparisonAttribute)) @@ -380,18 +354,4 @@ private static bool EvaluateSemVer(string s1, string s2, Comparator comparator) return false; } - - private readonly struct EvaluateResult - { - public EvaluateResult(JsonValue value, string? variationId, TRule matchedRule) - { - Value = value; - VariationId = variationId; - MatchedRule = matchedRule; - } - - public JsonValue Value { get; } - public string? VariationId { get; } - public TRule MatchedRule { get; } - } } diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index 292991e7..955cdad1 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -13,8 +13,8 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, ProjectConfig? remoteConfig) { var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; - return (EvaluationDetails)evaluator.Evaluate(setting, key, logDefaultValue, user, remoteConfig, - static (setting, value) => EvaluationDetails.Create(setting.SettingType, value, setting.UnsupportedTypeError)); + var evaluateResult = evaluator.Evaluate(setting, key, logDefaultValue, user); + return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, setting.UnsupportedTypeError, fetchTime: remoteConfig?.TimeStamp, user); } public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, IReadOnlyDictionary? settings, string key, T defaultValue, User? user, @@ -41,8 +41,8 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setti ProjectConfig? remoteConfig) { var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; - return evaluator.Evaluate(setting, key, logDefaultValue, user, remoteConfig, - static (setting, value) => EvaluationDetails.Create(setting.SettingType, value)); + var evaluateResult = evaluator.Evaluate(setting, key, logDefaultValue, user); + return EvaluationDetails.FromEvaluateResult(key, evaluateResult, setting.SettingType, setting.UnsupportedTypeError, fetchTime: remoteConfig?.TimeStamp, user); } public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, IReadOnlyDictionary? settings, User? user,