From 00c467d7be6c0775fae3695e160189e780492608 Mon Sep 17 00:00:00 2001 From: Endre T Date: Tue, 1 Sep 2020 23:46:44 +0200 Subject: [PATCH] Feature-variationid, Case Sensitive support, userID can be null (#13) * implement 'variation id' feature * add unit and integration test for variationid * RELEASE v5.3 Co-authored-by: andrew-cat --- README.md | 2 +- .../BasicConfigCatClientIntegrationTests.cs | 83 ++++++++++ .../ConfigCat.Client.Tests.csproj | 6 + .../ConfigCatClientTests.cs | 150 ++++++++++++++++++ .../ConfigEvaluatorTestsBase.cs | 3 +- .../ConfigServiceTests.cs | 74 ++++++++- src/ConfigCat.Client.Tests/UserTests.cs | 117 ++++++++------ .../VariationIdEvaluatorTests.cs | 64 ++++++++ .../data/sample_variationid_v4.json | 142 +++++++++++++++++ .../data/testmatrix_variationid.csv | 8 + src/ConfigCatClient/ConfigCatClient.cs | 94 +++++++++++ src/ConfigCatClient/ConfigCatClient.csproj | 4 +- .../Evaluate/EvaluateLogger.cs | 4 + .../Evaluate/EvaluateResult.cs | 17 ++ .../Evaluate/IRolloutEvaluator.cs | 2 + .../Evaluate/RolloutEvaluator.cs | 69 +++++--- src/ConfigCatClient/Evaluate/Setting.cs | 13 +- src/ConfigCatClient/IConfigCatClient.cs | 32 ++++ src/ConfigCatClient/User.cs | 18 ++- 19 files changed, 817 insertions(+), 85 deletions(-) create mode 100644 src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs create mode 100644 src/ConfigCat.Client.Tests/data/sample_variationid_v4.json create mode 100644 src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv create mode 100644 src/ConfigCatClient/Evaluate/EvaluateResult.cs diff --git a/README.md b/README.md index 62f5775d..4cd93a17 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ConfigCat is a hosted feature fl [![Build status](https://ci.appveyor.com/api/projects/status/3kygp783vc2uv9xr?svg=true)](https://ci.appveyor.com/project/ConfigCat/net-sdk) [![NuGet Version](https://buildstats.info/nuget/ConfigCat.Client)](https://www.nuget.org/packages/ConfigCat.Client/) [![codecov](https://codecov.io/gh/configcat/.net-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/configcat/.net-sdk) -![License](https://img.shields.io/github/license/configcat/.net-sdk.svg) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/configcat/.net-sdk/blob/master/LICENSE) ## Getting Started diff --git a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs index b0b7ecfe..b37216e5 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs @@ -127,5 +127,88 @@ private static async Task GetValueAsyncAndAssert(IConfigCatClient client, string Assert.AreEqual(expectedValue, actual); Assert.AreNotEqual(defaultValue, actual); } + + [TestMethod] + public void GetVariationId() + { + IConfigCatClient manualPollClient = ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(consoleLogger) + .WithManualPoll() + .Create(); + + manualPollClient.ForceRefresh(); + var actual = manualPollClient.GetVariationId("stringDefaultCat", "default"); + + Assert.AreEqual("7a0be518", actual); + } + + [TestMethod] + public async Task GetVariationIdAsync() + { + IConfigCatClient manualPollClient = ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(consoleLogger) + .WithManualPoll() + .Create(); + + await manualPollClient.ForceRefreshAsync(); + + var actual = await manualPollClient.GetVariationIdAsync("stringDefaultCat", "default"); + + Assert.AreEqual("7a0be518", actual); + } + + [TestMethod] + public void GetAllVariationId() + { + // Arrange + + const string expectedJsonString = "[\"7a0be518\",\"83372510\",\"2459598d\",\"ce564c3a\",\"44ab483a\",\"d227b334\",\"93f5a1c0\",\"bb66b1f3\",\"09513143\",\"489a16d2\",\"607147d5\",\"11634414\",\"faadbf54\",\"5af8acc7\",\"183ee713\",\"baff2362\"]"; + + var expectedValue = Newtonsoft.Json.JsonConvert.DeserializeObject(expectedJsonString); + + IConfigCatClient manualPollClient = ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(consoleLogger) + .WithManualPoll() + .Create(); + + manualPollClient.ForceRefresh(); + + // Act + + var actual = manualPollClient.GetAllVariationId(new User("a@configcat.com")); + + // Assert + Assert.AreEqual(16, expectedValue.Length); + CollectionAssert.AreEquivalent(expectedValue, actual.ToArray()); + } + + [TestMethod] + public async Task GetAllVariationIdAsync() + { + // Arrange + + const string expectedJsonString = "[\"7a0be518\",\"83372510\",\"2459598d\",\"ce564c3a\",\"44ab483a\",\"d227b334\",\"93f5a1c0\",\"bb66b1f3\",\"09513143\",\"489a16d2\",\"607147d5\",\"11634414\",\"faadbf54\",\"5af8acc7\",\"183ee713\",\"baff2362\"]"; + + var expectedValue = Newtonsoft.Json.JsonConvert.DeserializeObject(expectedJsonString); + + IConfigCatClient manualPollClient = ConfigCatClientBuilder + .Initialize(SDKKEY) + .WithLogger(consoleLogger) + .WithManualPoll() + .Create(); + + await manualPollClient.ForceRefreshAsync(); + + // Act + + var actual = await manualPollClient.GetAllVariationIdAsync(new User("a@configcat.com")); + + // Assert + Assert.AreEqual(16, expectedValue.Length); + CollectionAssert.AreEquivalent(expectedValue, actual.ToArray()); + } } } diff --git a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj index d1bcba81..29736a74 100644 --- a/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj +++ b/src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj @@ -41,6 +41,9 @@ Always + + Always + Always @@ -56,6 +59,9 @@ Always + + Always + diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index ce745d45..c8a1eb50 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -345,5 +345,155 @@ public void GetAllKeys_DeserializeFailed_ShouldReturnsWithEmptyArray() Assert.AreEqual(0, actualKeys.Count()); loggerMock.Verify(m => m.Warning(It.IsAny()), Times.Once); } + + [TestMethod] + public void GetVariationId_EvaluateServiceThrowException_ShouldReturnDefaultValue() + { + // Arrange + + const string defaultValue = "Victory for the Firstborn!"; + + evaluateMock + .Setup(m => m.EvaluateVariationId(It.IsAny(), It.IsAny(), defaultValue, null)) + .Throws(); + + var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + + // Act + + var actual = client.GetVariationId(null, defaultValue); + + // Assert + + Assert.AreEqual(defaultValue, actual); + } + + [TestMethod] + public async Task GetVariationIdAsync_EvaluateServiceThrowException_ShouldReturnDefaultValue() + { + // Arrange + + const string defaultValue = "Victory for the Firstborn!"; + + evaluateMock + .Setup(m => m.EvaluateVariationId(It.IsAny(), It.IsAny(), defaultValue, null)) + .Throws(); + + var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + + // Act + + var actual = await client.GetVariationIdAsync(null, defaultValue); + + // Assert + + Assert.AreEqual(defaultValue, actual); + } + + [TestMethod] + public void GetVariationId_DeserializeFailed_ShouldReturnsWithEmptyArray() + { + // Arrange + + var configServiceMock = new Mock(); + var loggerMock = new Mock(); + var evaluatorMock = new Mock(); + var configDeserializerMock = new Mock(); + + configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); + IDictionary o = new Dictionary(); + configDeserializerMock + .Setup(m => m.TryDeserialize(It.IsAny(), out o)) + .Returns(false); + + IConfigCatClient instance = new ConfigCatClient( + configServiceMock.Object, + loggerMock.Object, + evaluatorMock.Object, + configDeserializerMock.Object); + + // Act + + var actual = instance.GetAllVariationId(); + + // Assert + + Assert.IsNotNull(actual); + Assert.AreEqual(0, actual.Count()); + loggerMock.Verify(m => m.Warning(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task GetVariationIdAsync_DeserializeFailed_ShouldReturnsWithEmptyArray() + { + // Arrange + + var configServiceMock = new Mock(); + var loggerMock = new Mock(); + var evaluatorMock = new Mock(); + var configDeserializerMock = new Mock(); + + configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty); + IDictionary o = new Dictionary(); + configDeserializerMock + .Setup(m => m.TryDeserialize(It.IsAny(), out o)) + .Returns(false); + + IConfigCatClient instance = new ConfigCatClient( + configServiceMock.Object, + loggerMock.Object, + evaluatorMock.Object, + configDeserializerMock.Object); + + // Act + + var actual = await instance.GetAllVariationIdAsync(); + + // Assert + + Assert.IsNotNull(actual); + Assert.AreEqual(0, actual.Count()); + loggerMock.Verify(m => m.Warning(It.IsAny()), Times.Once); + } + + [TestMethod] + public void GetAllVariationId_ConfigServiceThrowException_ShouldReturnEmptyEnumerable() + { + // Arrange + + configService + .Setup(m => m.GetConfigAsync()) + .Throws(); + + var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + + // Act + + var actual = client.GetAllVariationId(null); + + // Assert + + Assert.AreEqual(Enumerable.Empty(), actual); + } + + [TestMethod] + public async Task GetAllVariationIdAsync_ConfigServiceThrowException_ShouldReturnEmptyEnumerable() + { + // Arrange + + configService + .Setup(m => m.GetConfigAsync()) + .Throws(); + + var client = new ConfigCatClient(configService.Object, loggerMock.Object, evaluateMock.Object, deserializerMock.Object); + + // Act + + var actual = await client.GetAllVariationIdAsync(null); + + // Assert + + Assert.AreEqual(Enumerable.Empty(), actual); + } } } diff --git a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs index 6f16dea4..45c724ce 100644 --- a/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs +++ b/src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs @@ -107,8 +107,9 @@ public async Task MatrixTest(Action assertation) } } + [TestCategory("MatrixTests")] [TestMethod] - public async Task GetValue_MatrixTests() + public async Task Run_MatrixTests() { await MatrixTest(AssertValue); } diff --git a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs index c2343cc3..c3f2a8cc 100644 --- a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs @@ -245,7 +245,7 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ShouldOnceInvokeCache } [TestMethod] - public async Task AutoPollConfigService_RefreshConfigAsync_ConfigCahged_ShouldRaiseEvent() + public async Task AutoPollConfigService_RefreshConfigAsync_ConfigChanged_ShouldRaiseEvent() { // Arrange @@ -281,6 +281,78 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ConfigCahged_ShouldRa Assert.AreEqual(1, eventChanged); } + [TestMethod] + public void AutoPollConfigService_Dispose_ShouldStopTimer() + { + // Arrange + + long counter = 0; + long e1, e2; + + this.cacheMock + .Setup(m => m.Get()) + .Returns(cachedPc); + + this.fetcherMock + .Setup(m => m.Fetch(cachedPc)) + .Callback(() => Interlocked.Increment(ref counter)) + .Returns(Task.FromResult(cachedPc)); + + var service = new AutoPollConfigService( + fetcherMock.Object, + cacheMock.Object, + TimeSpan.FromSeconds(0.2d), + TimeSpan.Zero, + loggerMock.Object, + true); + + // Act + Thread.Sleep(TimeSpan.FromSeconds(1)); + e1 = Interlocked.Read(ref counter); + service.Dispose(); + + // Assert + + Thread.Sleep(TimeSpan.FromSeconds(2)); + e2 = Interlocked.Read(ref counter); + Console.WriteLine(e2 - e1); + Assert.IsTrue(e2 - e1 <= 1); + } + + [TestMethod] + public void AutoPollConfigService_WithoutTimer_InvokeDispose_ShouldDisposeService() + { + // Arrange + + long counter = -1; + long e1; + + this.cacheMock + .Setup(m => m.Get()) + .Returns(cachedPc); + + this.fetcherMock + .Setup(m => m.Fetch(cachedPc)) + .Callback(() => Interlocked.Increment(ref counter)) + .Returns(Task.FromResult(cachedPc)); + + var service = new AutoPollConfigService( + fetcherMock.Object, + cacheMock.Object, + TimeSpan.FromSeconds(0.2d), + TimeSpan.Zero, + loggerMock.Object, + false); + + // Act + Thread.Sleep(TimeSpan.FromSeconds(1)); + e1 = Interlocked.Read(ref counter); + service.Dispose(); + + // Assert + Assert.AreEqual(-1, e1); + } + [TestMethod] public async Task ManualPollConfigService_GetConfigAsync_ShouldInvokeCacheGet() { diff --git a/src/ConfigCat.Client.Tests/UserTests.cs b/src/ConfigCat.Client.Tests/UserTests.cs index 477f78f3..4faa4ba2 100644 --- a/src/ConfigCat.Client.Tests/UserTests.cs +++ b/src/ConfigCat.Client.Tests/UserTests.cs @@ -7,40 +7,7 @@ namespace ConfigCat.Client.Tests public class UserTests { [TestMethod] - public void CreateUser() - { - var u0 = new User(null); - - var u1 = new User("12345") - { - Email = "email", - Country = "US", - Custom = - { - { "key", "value"} - } - }; - - var u2 = new User("sw") - { - Email = null, - Country = "US", - Custom = - { - { "key0", "value"}, - { "key1", "value"}, - { "key2", "value"}, - } - }; - - var u3 = new User("sw"); - - u3.Custom.Add("customKey0", "customValue"); - u3.Custom["customKey1"] = "customValue"; - } - - [TestMethod] - public void UseCustomProperties() + public void CreateUser_WithIdAndEmailAndCountry_AllAttributesShouldContainsPassedValues() { // Arrange @@ -58,18 +25,16 @@ public void UseCustomProperties() // Assert string s; - Assert.IsTrue(actualAttributes.TryGetValue("email", out s)); + Assert.IsTrue(actualAttributes.TryGetValue(nameof(User.Email), out s)); Assert.AreEqual("id@example.com", s); s = null; - Assert.IsTrue(actualAttributes.TryGetValue("country", out s)); + Assert.IsTrue(actualAttributes.TryGetValue(nameof(User.Country), out s)); Assert.AreEqual("US", s); s = null; - Assert.IsTrue(actualAttributes.TryGetValue("identifier", out s)); + Assert.IsTrue(actualAttributes.TryGetValue(nameof(User.Identifier), out s)); Assert.AreEqual("id", s); - - Assert.AreEqual(3, actualAttributes.Count); } [TestMethod] @@ -85,10 +50,10 @@ public void UseWellKnownAttributesAsCustomProperties_ShouldNotAppendAllAttribute Custom = new Dictionary { - { "myCustomAttribute", ""}, - { "identifier", "myIdentifier"}, - { "country", "United States"}, - { "email", "otherEmail@example.com"} + { "myCustomAttribute", "myCustomAttributeValue"}, + { nameof(User.Identifier), "myIdentifier"}, + { nameof(User.Country), "United States"}, + { nameof(User.Email), "otherEmail@example.com"} } }; @@ -98,21 +63,69 @@ public void UseWellKnownAttributesAsCustomProperties_ShouldNotAppendAllAttribute // Assert - Assert.AreEqual(4, actualAttributes.Count); - - string s; - Assert.IsTrue(actualAttributes.TryGetValue("identifier", out s)); + Assert.IsTrue(actualAttributes.TryGetValue(nameof(User.Identifier), out string s)); Assert.AreEqual("id", s); + Assert.AreNotEqual("myIdentifier", s); - s = null; - Assert.IsTrue(actualAttributes.TryGetValue("country", out s)); + Assert.IsTrue(actualAttributes.TryGetValue(nameof(User.Country), out s)); Assert.AreEqual("US", s); Assert.AreNotEqual("United States", s); - - s = null; - Assert.IsTrue(actualAttributes.TryGetValue("email", out s)); + + Assert.IsTrue(actualAttributes.TryGetValue(nameof(User.Email), out s)); Assert.AreEqual("id@example.com", s); Assert.AreNotEqual("otherEmail@example.com", s); + + Assert.AreEqual(4, actualAttributes.Count); + } + + [DataTestMethod] + [DataRow("identifier", "myId")] + [DataRow("IDENTIFIER", "myId")] + [DataRow("email", "theBoss@example.com")] + [DataRow("EMAIL", "theBoss@example.com")] + [DataRow("eMail", "theBoss@example.com")] + [DataRow("country", "myHome")] + [DataRow("COUNTRY", "myHome")] + public void UseWellKnownAttributesAsCustomPropertiesWithDifferentNames_ShouldAppendAllAttributes(string attributeName, string attributeValue) + { + // Arrange + + var user = new User("id") + { + Email = "id@example.com", + + Country = "US", + + Custom = new Dictionary + { + { attributeName, attributeValue} + } + }; + + // Act + + var actualAttributes = user.AllAttributes; + + // Assert + + Assert.AreEqual(4, actualAttributes.Count); + + Assert.IsTrue(actualAttributes.TryGetValue(attributeName, out string s)); + Assert.AreEqual(attributeValue, s); + } + + [DataTestMethod()] + [DataRow(null, User.DefaultIdentifierValue)] + [DataRow("", User.DefaultIdentifierValue)] + [DataRow("id", "id")] + [DataRow("\t", "\t")] + [DataRow("\u1F600", "\u1F600")] + public void CreateUser_ShouldSetIdentifier(string identifier, string expectedValue) + { + var user = new User(identifier); + + Assert.AreEqual(expectedValue, user.Identifier); + Assert.AreEqual(expectedValue, user.AllAttributes[nameof(User.Identifier)]); } } } diff --git a/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs b/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs new file mode 100644 index 00000000..3dcc235a --- /dev/null +++ b/src/ConfigCat.Client.Tests/VariationIdEvaluatorTests.cs @@ -0,0 +1,64 @@ +using ConfigCat.Client.Evaluate; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Collections.Generic; + +namespace ConfigCat.Client.Tests +{ + [TestClass] + public class VariationIdEvaluatorTests : ConfigEvaluatorTestsBase + { + protected override string SampleJsonFileName => "sample_variationid_v4.json"; + + protected override string MatrixResultFileName => "testmatrix_variationid.csv"; + + protected override void AssertValue(string keyName, string expected, User user) + { + var actual = base.configEvaluator.EvaluateVariationId(base.config, keyName, null, user); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void EvaluateVariationId_WithSimpleKey_ShouldReturnCat() + { + string actual = configEvaluator.EvaluateVariationId(base.config, "boolean", string.Empty); + + Assert.AreNotEqual(string.Empty, actual); + Assert.AreEqual("a0e56eda", actual); + } + + [TestMethod] + public void EvaluateVariationId_WithNonExistingKey_ShouldReturnDefaultValue() + { + string actual = configEvaluator.EvaluateVariationId(config, "NotExistsKey", "DefaultVariationId"); + + Assert.AreEqual("DefaultVariationId", actual); + } + + [TestMethod] + public void EvaluateVariationId_WithEmptyProjectConfig_ShouldReturnDefaultValue() + { + string actual = configEvaluator.EvaluateVariationId(ProjectConfig.Empty, "stringDefaultCat", "Default"); + + Assert.AreEqual("Default", actual); + } + + [TestMethod] + public void EvaluateVariationId_WithUser_ShouldReturnEvaluatedValue() + { + var actual = configEvaluator.EvaluateVariationId( + config, + "text", + "defaultVariationId", + new User("bryanw@verizon.net") + { + Email = "bryanw@verizon.net", + Country = "Hungary" + }); + + Assert.AreEqual("30ba32b9", actual); + } + } +} diff --git a/src/ConfigCat.Client.Tests/data/sample_variationid_v4.json b/src/ConfigCat.Client.Tests/data/sample_variationid_v4.json new file mode 100644 index 00000000..244f1b6a --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/sample_variationid_v4.json @@ -0,0 +1,142 @@ +{ + "boolean": { + "v": false, + "i": "a0e56eda", + "t": 0, + "p": [ + { + "o": 0, + "v": true, + "p": 50, + "i": "67787ae4" + }, + { + "o": 1, + "v": false, + "p": 50, + "i": "a0e56eda" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": true, + "i": "67787ae4" + } + ] + }, + "text": { + "v": "c", + "t": 1, + "i": "3f05be89", + "p": [ + { + "o": 0, + "v": "a", + "p": 50, + "i": "30ba32b9" + }, + { + "o": 1, + "v": "b", + "p": 50, + "i": "cf19e913" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": "true", + "i": "9bdc6a1f" + }, + { + "o": 1, + "a": "Email", + "t": 2, + "c": "@test.com", + "v": "false", + "i": "65310deb" + } + ] + }, + "whole": { + "v": 999999, + "i": "cf2e9162", + "t": 2, + "p": [ + { + "o": 0, + "v": 0, + "p": 50, + "i": "ec14f6a9" + }, + { + "o": 1, + "v": -1, + "p": 50, + "i": "61a5a033" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": 1, + "i": "ab30533b" + } + ] + }, + "decimal": { + "v": 0.0, + "i": "63612d39", + "t": 3, + "p": [ + { + "o": 0, + "v": 1.0, + "p": 50, + "i": "d0dbc27f" + }, + { + "o": 1, + "v": 2.0, + "p": 50, + "i": "8155ad7b" + } + ], + "r": [ + { + "o": 0, + "a": "Email", + "t": 2, + "c": "@configcat.com", + "v": -2147483647.2147484, + "i": "8f9559cf" + }, + { + "o": 1, + "a": "Email", + "t": 0, + "c": "a@test.com", + "v": 0.12345678912345678, + "i": "d66c5781" + }, + { + "o": 2, + "a": "Email", + "t": 0, + "c": "b@test.com", + "v": 0.12345678912, + "i": "d66c5781" + } + ] + } +} \ No newline at end of file diff --git a/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv b/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv new file mode 100644 index 00000000..0d2a7b7d --- /dev/null +++ b/src/ConfigCat.Client.Tests/data/testmatrix_variationid.csv @@ -0,0 +1,8 @@ +Identifier;Email;Country;Custom1;boolean;decimal;text;whole +##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162; +a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; +b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; +a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9; +b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9; +cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9; +bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; \ No newline at end of file diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index bf9af72c..133f52a9 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -2,6 +2,7 @@ using ConfigCat.Client.Evaluate; using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; @@ -213,6 +214,98 @@ public void Dispose() } } + /// + public string GetVariationId(string key, string defaultVariationId, User user = null) + { + try + { + var c = this.configService.GetConfigAsync().Result; + + return this.configEvaluator.EvaluateVariationId(c, key, defaultVariationId, user); + } + catch (Exception ex) + { + this.log.Error($"Error occured in 'GetVariationId' method.\n{ex}"); + + return defaultVariationId; + } + } + + /// + public async Task GetVariationIdAsync(string key, string defaultVariationId, User user = null) + { + try + { + var c = await this.configService.GetConfigAsync().ConfigureAwait(false); + + return this.configEvaluator.EvaluateVariationId(c, key, defaultVariationId, user); + } + catch (Exception ex) + { + this.log.Error($"Error occured in 'GetVariationIdAsync' method.\n{ex}"); + + return defaultVariationId; + } + } + + /// + public IEnumerable GetAllVariationId(User user = null) + { + try + { + var c = this.configService.GetConfigAsync().Result; + + return GetAllVariationIdLogic(c, user); + } + catch (Exception ex) + { + this.log.Error($"Error occured in 'GetAllVariationId' method.\n{ex}"); + } + + return Enumerable.Empty(); + } + + /// + public async Task> GetAllVariationIdAsync(User user = null) + { + try + { + var c = await this.configService.GetConfigAsync().ConfigureAwait(false); + + return GetAllVariationIdLogic(c, user); + } + catch (Exception ex) + { + this.log.Error($"Error occured in 'GetAllVariationIdAsync' method.\n{ex}"); + } + + return Enumerable.Empty(); + } + + private IEnumerable GetAllVariationIdLogic(ProjectConfig config, User user) + { + if (this.configDeserializer.TryDeserialize(config, out var settings)) + { + var result = new List(settings.Keys.Count); + + foreach (var key in settings.Keys) + { + var r = this.configEvaluator.EvaluateVariationId(config, key, null, user); + + if (r != null) + { + result.Add(r); + } + } + + return result; + } + + this.log.Warning("Config deserialization failed."); + + return Enumerable.Empty(); + } + /// /// Create a instance to setup the client /// @@ -222,5 +315,6 @@ public static ConfigCatClientBuilder Create(string sdkKey) { return ConfigCatClientBuilder.Initialize(sdkKey); } + } } \ No newline at end of file diff --git a/src/ConfigCatClient/ConfigCatClient.csproj b/src/ConfigCatClient/ConfigCatClient.csproj index 6ce4a2ad..c38298e7 100644 --- a/src/ConfigCatClient/ConfigCatClient.csproj +++ b/src/ConfigCatClient/ConfigCatClient.csproj @@ -15,7 +15,9 @@ https://github.com/ConfigCat/.net-sdk https://github.com/ConfigCat/.net-sdk git - Version 5.2.0 + Version 5.3.0 + * VariationID, bugfix (#11) +Version 5.2.0 * Bugfix (config fetch, caching) Version 5.1.0 * Remove semver nuget packages diff --git a/src/ConfigCatClient/Evaluate/EvaluateLogger.cs b/src/ConfigCatClient/Evaluate/EvaluateLogger.cs index 29158709..6a6dc3e9 100644 --- a/src/ConfigCatClient/Evaluate/EvaluateLogger.cs +++ b/src/ConfigCatClient/Evaluate/EvaluateLogger.cs @@ -13,6 +13,8 @@ internal sealed class EvaluateLogger public ICollection Operations { get; private set; } = new List(); + public string VariationId { get; set; } + public void Log(string message) { this.Operations.Add(message); @@ -24,6 +26,8 @@ public override string ToString() result.AppendLine($" Evaluate '{KeyName}'"); + result.AppendLine($" VariationId: {this.VariationId ?? "null"}"); + result.AppendLine($" User object: {Newtonsoft.Json.JsonConvert.SerializeObject(this.User)}"); foreach (var o in this.Operations) diff --git a/src/ConfigCatClient/Evaluate/EvaluateResult.cs b/src/ConfigCatClient/Evaluate/EvaluateResult.cs new file mode 100644 index 00000000..20178eba --- /dev/null +++ b/src/ConfigCatClient/Evaluate/EvaluateResult.cs @@ -0,0 +1,17 @@ +namespace ConfigCat.Client.Evaluate +{ + internal class EvaluateResult + { + public string RawValue { get; set; } + + public string VariationId { get; set; } + + public EvaluateResult() : this(null, null) { } + + public EvaluateResult(string rawValue, string variationId) + { + this.RawValue = rawValue; + this.VariationId = variationId; + } + } +} \ No newline at end of file diff --git a/src/ConfigCatClient/Evaluate/IRolloutEvaluator.cs b/src/ConfigCatClient/Evaluate/IRolloutEvaluator.cs index 2691d748..dc1b865f 100644 --- a/src/ConfigCatClient/Evaluate/IRolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluate/IRolloutEvaluator.cs @@ -3,5 +3,7 @@ internal interface IRolloutEvaluator { T Evaluate(ProjectConfig projectConfig, string key, T defaultValue, User user = null); + + string EvaluateVariationId(ProjectConfig projectConfig, string key, string defaultVariationId, User user = null); } } \ No newline at end of file diff --git a/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs index c55876d3..6deda560 100644 --- a/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluate/RolloutEvaluator.cs @@ -21,33 +21,58 @@ public RolloutEvaluator(ILogger logger, IConfigDeserializer configDeserializer) } public T Evaluate(ProjectConfig projectConfig, string key, T defaultValue, User user = null) + { + var result = EvaluateLogic(projectConfig, key, defaultValue?.ToString(), null, user); + + if (result == null) + { + return defaultValue; + } + + return new JValue(result.RawValue).Value(); + } + + public string EvaluateVariationId(ProjectConfig projectConfig, string key, string defaultVariationId, User user = null) + { + var result = EvaluateLogic(projectConfig, key, null, defaultVariationId, user); + + if (result == null) + { + return defaultVariationId; + } + + return result.VariationId; + } + + private EvaluateResult EvaluateLogic(ProjectConfig projectConfig, string key, string logDefaultValue, string logDefaultVariationId, User user = null) { if (!this.configDeserializer.TryDeserialize(projectConfig, out var settings)) { this.log.Warning("Config deserialization failed, returning defaultValue"); - return defaultValue; + return null; } if (!settings.TryGetValue(key, out var setting)) { var keys = string.Join(",", settings.Keys.Select(s => $"'{s}'").ToArray()); - this.log.Error($"Evaluating '{key}' failed. Returning default value: '{defaultValue}'. Here are the available keys: {keys}."); + this.log.Error($"Evaluating '{key}' failed. Returning default value: '{logDefaultValue}'. Here are the available keys: {keys}."); - return defaultValue; + return null; } - var evaluateLog = new EvaluateLogger + var evaluateLog = new EvaluateLogger { - ReturnValue = defaultValue, + ReturnValue = logDefaultValue, User = user, - KeyName = key + KeyName = key, + VariationId = logDefaultVariationId }; try { - T result; + EvaluateResult result = null; if (user != null) { @@ -55,7 +80,9 @@ public T Evaluate(ProjectConfig projectConfig, string key, T defaultValue, Us if (TryEvaluateRules(setting.RolloutRules, user, evaluateLog, out result)) { - evaluateLog.ReturnValue = result; + evaluateLog.ReturnValue = result.RawValue; + evaluateLog.VariationId = result.VariationId; + return result; } @@ -64,7 +91,8 @@ public T Evaluate(ProjectConfig projectConfig, string key, T defaultValue, Us if (TryEvaluateVariations(setting.RolloutPercentageItems, key, user, out result)) { evaluateLog.Log("evaluate % option => user targeted"); - evaluateLog.ReturnValue = result; + evaluateLog.ReturnValue = result.RawValue; + evaluateLog.VariationId = result.VariationId; return result; } @@ -80,9 +108,10 @@ public T Evaluate(ProjectConfig projectConfig, string key, T defaultValue, Us // regular evaluate - result = new JValue(setting.Value).Value(); + result = new EvaluateResult(setting.RawValue, setting.VariationId); - evaluateLog.ReturnValue = result; + evaluateLog.ReturnValue = result.RawValue; + evaluateLog.VariationId = result.VariationId; return result; } @@ -92,9 +121,9 @@ public T Evaluate(ProjectConfig projectConfig, string key, T defaultValue, Us } } - private bool TryEvaluateVariations(ICollection rolloutPercentageItems, string key, User user, out T result) + private bool TryEvaluateVariations(ICollection rolloutPercentageItems, string key, User user, out EvaluateResult result) { - result = default(T); + result = new EvaluateResult(); if (rolloutPercentageItems != null && rolloutPercentageItems.Count > 0) { @@ -112,7 +141,8 @@ private bool TryEvaluateVariations(ICollection rollout if (hashScale < bucket) { - result = new JValue(variation.RawValue).Value(); + result.RawValue = variation.RawValue; + result.VariationId = variation.VariationId; return true; } @@ -122,22 +152,23 @@ private bool TryEvaluateVariations(ICollection rollout return false; } - private static bool TryEvaluateRules(ICollection rules, User user, EvaluateLogger logger, out T result) + private static bool TryEvaluateRules(ICollection rules, User user, EvaluateLogger logger, out EvaluateResult result) { - result = default(T); + result = new EvaluateResult(); if (rules != null && rules.Count > 0) { foreach (var rule in rules.OrderBy(o => o.Order)) { - result = new JValue(rule.RawValue).Value(); + result.RawValue = rule.RawValue; + result.VariationId = rule.VariationId; - if (!user.AllAttributes.ContainsKey(rule.ComparisonAttribute.ToLowerInvariant())) + if (!user.AllAttributes.ContainsKey(rule.ComparisonAttribute)) { continue; } - var comparisonAttributeValue = user.AllAttributes[rule.ComparisonAttribute.ToLowerInvariant()]; + var comparisonAttributeValue = user.AllAttributes[rule.ComparisonAttribute]; if (string.IsNullOrEmpty(comparisonAttributeValue)) { continue; diff --git a/src/ConfigCatClient/Evaluate/Setting.cs b/src/ConfigCatClient/Evaluate/Setting.cs index 1d172846..f5a93cc5 100644 --- a/src/ConfigCatClient/Evaluate/Setting.cs +++ b/src/ConfigCatClient/Evaluate/Setting.cs @@ -6,7 +6,7 @@ namespace ConfigCat.Client.Evaluate internal class Setting { [JsonProperty(PropertyName = "v")] - public string Value { get; set; } + public string RawValue { get; set; } [JsonProperty(PropertyName = "t")] public SettingTypeEnum SettingType { get; set; } @@ -16,6 +16,9 @@ internal class Setting [JsonProperty(PropertyName = "r")] public List RolloutRules { get; set; } + + [JsonProperty(PropertyName = "i")] + public string VariationId { get; set; } } internal class RolloutPercentageItem @@ -27,7 +30,10 @@ internal class RolloutPercentageItem public string RawValue { get; private set; } [JsonProperty(PropertyName = "p")] - public int Percentage { get; set; } + public int Percentage { get; set; } + + [JsonProperty(PropertyName = "i")] + public string VariationId { get; set; } } internal class RolloutRule @@ -46,6 +52,9 @@ internal class RolloutRule [JsonProperty(PropertyName = "v")] public string RawValue { get; private set; } + + [JsonProperty(PropertyName = "i")] + public string VariationId { get; set; } } internal enum SettingTypeEnum : byte diff --git a/src/ConfigCatClient/IConfigCatClient.cs b/src/ConfigCatClient/IConfigCatClient.cs index 1277cfb7..3bdb83ed 100644 --- a/src/ConfigCatClient/IConfigCatClient.cs +++ b/src/ConfigCatClient/IConfigCatClient.cs @@ -55,5 +55,37 @@ public interface IConfigCatClient : IDisposable /// Refresh the configuration /// Task ForceRefreshAsync(); + + /// + /// Returns the Variation ID (analytics) of a feature flag or setting by the given key. + /// + /// Key for programs + /// In case of failure return this value + /// The user object for variation evaluation + /// Variation ID + string GetVariationId(string key, string defaultVariationId, User user = null); + + /// + /// Returns the Variation ID (analytics) of a feature flag or setting by the given key. + /// + /// Key for programs + /// In case of failure return this value + /// The user object for variation evaluation + /// Variation ID + Task GetVariationIdAsync(string key, string defaultVariationId, User user = null); + + /// + /// Returns Variation IDs (analytics) of all feature flags or settings. + /// + /// The user object for variation evaluation + /// Collection of all Variation IDs + IEnumerable GetAllVariationId(User user = null); + + /// + /// Returns Variation IDs (analytics) of all feature flags or settings. + /// + /// The user object for variation evaluation + /// Collection of all Variation IDs + Task> GetAllVariationIdAsync(User user = null); } } \ No newline at end of file diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index 24f44bf0..b1b8f2e5 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -7,6 +7,8 @@ namespace ConfigCat.Client /// public class User { + internal const string DefaultIdentifierValue = ""; + /// /// Unique identifier for the User or Session. e.g. Email address, Primary key, Session Id /// @@ -37,23 +39,23 @@ public IReadOnlyDictionary AllAttributes { var result = new Dictionary { - { "identifier", this.Identifier}, - { "email", this.Email}, - { "country", this.Country}, + { nameof(Identifier), this.Identifier}, + { nameof(Email), this.Email}, + { nameof(Country), this.Country}, }; if (Custom != null && Custom.Count > 0) { foreach (var item in this.Custom) { - if (item.Key.ToLowerInvariant() == nameof(Identifier).ToLowerInvariant() || - item.Key.ToLowerInvariant() == nameof(Email).ToLowerInvariant() || - item.Key.ToLowerInvariant() == nameof(Country).ToLowerInvariant()) + if (item.Key == nameof(Identifier) || + item.Key == nameof(Email) || + item.Key == nameof(Country)) { continue; } - result.Add(item.Key.ToLowerInvariant(), item.Value); + result.Add(item.Key, item.Value); } } @@ -67,7 +69,7 @@ public IReadOnlyDictionary AllAttributes /// Unique identifier for the User public User(string identifier) { - this.Identifier = identifier; + this.Identifier = string.IsNullOrEmpty(identifier) ? DefaultIdentifierValue : identifier; this.Custom = new Dictionary(0); } }